buto > /dev/null

だいたい急に挑戦してゴールにたどり着かずに飽きる日々です

react setState反映タイミングを調べてみる

昨日の記事でMaterial-UIのTableを使ってオブジェクト配列を持つstate変数の値を表示しましたが

最初にsetState()でセットしたオブジェクトが表示されていませんでした

components/DataTable.tsx

useItem.tsxで取得したstate変数をDataTable.tsxで定義したstate変数にセットし直しています

useEffectでログ出力を追加しました

import {useState, useEffect} from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import useItem from '../logic/useItem';
// オブジェクト型をインポート
import type {Item} from '../logic/useItem';

function DataTable() {

  const {itemList, setItemList, addItem} = useItem();
  const [saleItem, setSaleItem] = useState<Item[]>([]);

  const sample: Item = {
    id: 0,
    category: 'none',
    item: 'sampleItem',
    price: 0
  };
  let array: Item[] = [];
  array.push(sample);

  useEffect(() => {
    setItemList(array);
    console.log('useEffect([])');
    console.log('itemList : ' + JSON.stringify(itemList));
    console.log('**************');
    // regularItem();
  }, []);

  useEffect(() => {
    setSaleItem(itemList);
    console.log('useEffect([itemList])');
    console.log('itemList : ' + JSON.stringify(itemList));
    console.log('saleItem' + JSON.stringify(saleItem));
    console.log('**************');
  }, [itemList]);

  const regularItem = () => {
    addItem('sweets', 'chocolate', 250);
    addItem('sweets', 'muffin', 390);
    addItem('snack', 'potato chips', 180);
    addItem('beverage', 'cola', 120);
  };

  return (
    <TableContainer>
      <Table sx={{ maxWidth: 250 }}>
        <TableHead>
          <TableRow>
            <TableCell>id</TableCell>
            <TableCell>category</TableCell>
            <TableCell>item</TableCell>
            <TableCell>price(¥)</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {saleItem.map(item => (
            <TableRow key={item.id}>
              <TableCell align="right">{item.id}</TableCell>
              <TableCell>{item.category}</TableCell>
              <TableCell>{item.item}</TableCell>
              <TableCell align="right">{item.price}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

export default DataTable;

logic/useItem.tsx ※昨日と同じです

import {useState} from 'react';

function useItem() {

  const [itemList, setItemList] = useState<Item[]>([]);

  const addItem = (category:string, item:string, price:number) => {
    const el:Item = {
      id: itemList.length,
      category: category,
      item: item,
      price: Math.ceil(price * 1.1)
    };

    const current = itemList;
    current.push(el);
    setItemList(current);
  };

  return {itemList, setItemList, addItem};
};

export default useItem;

/* 商品のオブジェクト型 */
type Item = {
  id: number,
  category: string,
  item: string,
  price: number
};

export type {Item};

初期表示で変数sampleの値が表示されています f:id:butorisa:20210922234325p:plain

ログを読み解くと、、、

  1. 第2引数が[]のuseEffect()が実行され、useItem.tsxのitemListに値がセットされる
  2. この時点でのitemListは値がセットされる前の状態なので[]
  3. 第2引数が[itemList]のuseEffect()が実行される
  4. saleItemはもちろん[]
  5. itemListが変更されたので第2引数が[itemList]のuseEffect()が実行される
  6. DataTable.tsxのsaleItemに値がセットされる
  7. itemListは値がセットされた後の状態になっている
  8. この時点でのsaleItemは値がセットされる前の状態なので[]

state変数の変更反映はレンダリング後なのでstate変数を他の変数に代入し直して表示させる必要があります

react typeで型定義して使い回す

typeでオブジェクト型を定義する

typescriptではtypeキーワードで作成したオブジェクトを型として扱うことができます

「key: プリミティブ型」の書き方で定義していきます

type Item = {
  id: number,
  category: string,
  item: string,
  price: number
};

APIのレスポンスをtypeで型定義しておくと型チェックがされるのでケアレスミスが防げそう!

typeインポート&エクスポート

定義した型はエクスポートすると他のtsxファイル(別コンポーネント)でもインポートして使うことができます

export type {Item};
import type {Item} from '../logic/useItem';

アプリで使うtype一覧をNameSpaceを使って1つのファイルに定義しているプロジェクトも見たのですが、

「NS.Item」のようにNameSpaceを指定する必要があるのと(ちなみにそのプロジェクトではNameSpacehaNSしかなかった)

型一覧を1ファイルにまとめてしまうことで1つ1つの型がどんな画面、処理で使われているか分かりにくく感じました

なので型定義は型を使うロジック関数のファイルに定義しています

typeを使ってテーブルにデータを表示

Material-UIのTableに商品のデータを表示します

components/DataTable.tsx

import {useState, useEffect} from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import useItem from '../logic/useItem';
// オブジェクト型をインポート
import type {Item} from '../logic/useItem';

function DataTable() {

  const {itemList, setItemList, addItem} = useItem();
  const [saleItem, setSaleItem] = useState<Item[]>([]);

  const sample: Item = {
    id: 0,
    category: 'none',
    item: 'sampleItem',
    price: 0
  };

  useEffect(() => {
    setItemList([sample]);
    regularItem();
    setSaleItem(itemList);
  }, []);

  const regularItem = () => {
    addItem('sweets', 'chocolate', 250);
    addItem('sweets', 'muffin', 390);
    addItem('snack', 'potato chips', 180);
    addItem('beverage', 'cola', 120);
  };

  return (
    <TableContainer>
      <Table sx={{ maxWidth: 250 }}>
        <TableHead>
          <TableRow>
            <TableCell>id</TableCell>
            <TableCell>category</TableCell>
            <TableCell>item</TableCell>
            <TableCell>price(¥)</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {saleItem.map(item => (
            <TableRow>
              <TableCell align="right">{item.id}</TableCell>
              <TableCell>{item.category}</TableCell>
              <TableCell>{item.item}</TableCell>
              <TableCell align="right">{item.price}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

export default DataTable;

後述のlogic/useItem.tsxで定義した「Item型」の配列データを表示します

App.tsx

import Box from '@mui/material/Box';
import Header from './components/Header';
import DataTable from './components/DataTable';

function App() {

  return (
    <div>
      <Box
        sx={{
          padding:1,
          margin:1
        }}
      >
        <Header/>
      </Box>
      <Box
        sx={{
          padding:1,
          margin:1
        }}
      >
        <DataTable/>
      </Box>
    </div>
  );
}

export default App;

※Header.tsxはAppBarだけなので割愛します

logic/useItem.tsx

import {useState} from 'react';

function useItem(){

  const [itemList, setItemList] = useState<Item[]>([]);

  const addItem = (category:string, item:string, price:number) => {
    const el:Item = {
      id: itemList.length,
      category: category,
      item: item,
      price: Math.ceil(price * 1.1)
    };
    const current = itemList;
    current.push(el);
    setItemList(current);
  };

  return {itemList, setItemList, addItem};
}

export default useItem;

/* 商品のオブジェクト型 */
type Item = {
  id: number,
  category: string,
  item: string,
  price: number
};

export type {Item};

Item型の定義とstate変数の型指定をしています

Item型のデータが表示されました!(変数sampleで定義したデータが入っていない?) f:id:butorisa:20210922001041p:plain

react Material-UI入門

Material-UIはreactで利用できるデザインライブラリです

早速インストールして使ってみます!

yarn add @material-ui/core@next @emotion/react @emotion/styled

could not find a declaration file for module 'react/jsx-runtime'

上記コマンドでMaterial-UIをインストールすると「could not find a declaration…」のエラーが全部のtsxファイルで発生してしまった

import React from 'react'; でreactを読み込んでいますがreactの型定義が見つからないためエラーとなったようです

react自体はtypescript必須ではないので型定義を別途インストールしたら解決しました

yarn add -D @types/react

App Bar

シンプルなヘッダーができました f:id:butorisa:20210920161649p:plain

components/Header.tsx

import AppBar from '@mui/material/AppBar';

function Header() {

  return (
    <AppBar
      color="secondary"
    >
      buto {`>`} /dev/null
    </AppBar>
  );
}

export default Header;

Text Field & Button

components/InputArea.tsx

import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';

function InputArea() {

  return (
    <div>
      <TextField label="enter your name"/>
      <Button>enter</Button>
    </div>
  );
}

export default InputArea;

App.tsx

import Header from './components/Header';
import InputArea from './components/InputArea';

function App() {

  return (
    <div>
      <Header/>
      <InputArea/>
    </div>
  );
}

export default App;

ヘッダーと重なっちゃいました… f:id:butorisa:20210920202134p:plain

FlexBoxを使おう!

sx={{ padding:1,margin:1 }} という感じでsxプロパティにスタイル要素を記述します

App.tsx

import Box from '@mui/material/Box';
import Header from './components/Header';
import InputArea from './components/InputArea';

function App() {

  return (
    <div>
      <Box
        sx={{
          padding:1,
          margin:1
        }}
      >
        <Header/>
      </Box>
      <Box
        sx={{
          padding:1,
          margin:1
        }}
      >
        <InputArea/>
      </Box>
    </div>
  );
}

export default App;

AppBarとTextFieldを別々のBoxにしたので重ならずに表示されました! f:id:butorisa:20210920210122p:plain

Buttonの配置が上すぎるのでTextFieldとbuttonもFlexBoxで囲みます

Buttonの位置が分かりやすいように variant=contained にします

components/InputArea.tsx

import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';

function InputArea() {

  return (
    <Box
      sx={{
        display:"flex"
      }}
    >
      <Box>
        <TextField label="enter your name"/>
      </Box>
      <Box
        sx={{
          padding:1
        }}
      >
        <Button
          variant="contained"
          color="secondary"
        >
          enter
        </Button>
      </Box>
    </Box>
  );
}

export default InputArea;

TextFieldとButtonが中央揃えになりました! f:id:butorisa:20210920212811p:plain

参考

【TypeScript】【React】Could not find a declaration file for module ‘react’.の対処について

Theme UI-the sx Prop

Material UI-CSS Grid

react useEffectでライフサイクル入門

今日はuseEffectフックを使って関数を実行するタイミングを操作します

useEffectフック

useEffect(() => {実行したい処理}, [state変数]);

このように書くとstate変数が変更されたタイミングで実行したい処理が実行されます

第2引数([state変数]のところ)を変えることでstate変数の変更タイミング以外でも処理を実行することができます

レンダリングごとに実行(Updating)

初期表示+再描画の度に実行したい場合は「useEffectの第2引数を省略」します

サンプルコードではWelcome.tsxで名前を入力、Enterボタンを押下するとメッセージと現在日時が表示されます

useEffectには現在日時を取得してセットする処理を書いています

components/Welcome.tsx

import {useState,useEffect} from 'react';
import {useName} from '../logic/useName';

function Welcome() {

  const {disp, changeName} = useName();
  const [show, setShow] = useState(false);
  const [date, setDate] = useState('');

  // useEffectの第2引数を省略すると描画ごとに実行される
  useEffect(() => {
    const now: string = new Date().toLocaleString();
    setDate(now);
  });

  return (
    <div>
      <p>名前を入力してください</p>
      <input type="text" onBlur={e => changeName(e.target.value)}/>
      <button onClick={() => setShow(true)}>Enter</button>
      {show &&
        <div>
          <p>ようこそ!! {disp}</p>
          <p>{date}</p>
        </div>
      }
    </div>
  );
}

export default Welcome;

App.tsx

import Welcome from './components/Welcome';

function App() {

  return (
    <div>
      <Welcome/>
    </div>
  );
}

export default App;

logic/useName.tsx 入力された名前のsetter/getter

import {useState,useEffect} from 'react';

export const useName = () => {

  const [name, setName] = useState<string>('risa');
  const [disp, setDisp] = useState<string>(name);

  useEffect(() => {
    setDisp(name + 'さん');
  }, [name]);

  const changeName = (input:string) => {
    setName(input);
  };

  return {disp, changeName};
}

再描画のタイミング「名前を変更してテキストボックスからフォーカスアウトした時」に現在日時も変わっています f:id:butorisa:20210919171613g:plain

初期表示のみ実行(Mounting)

初期表示のみ実行したい場合は「useEffectの第2引数を[]」にします

components/Welcome.tsx

import {useState,useEffect} from 'react';
import {useName} from '../logic/useName';

function Welcome() {

  const {disp, changeName} = useName();
  const [show, setShow] = useState(false);
  const [date, setDate] = useState('');

  // useEffectの第2引数を[](空配列)すると初期表示のみ実行される
  useEffect(() => {
    const now: string = new Date().toLocaleString();
    setDate(now);
  }, []);

  return (
    <div>
      <p>名前を入力してください</p>
      <input type="text" onBlur={e => changeName(e.target.value)}/>
      <button onClick={() => setShow(true)}>Enter</button>
      {show &&
        <div>
          <p>ようこそ!! {disp}</p>
          <p>{date}</p>
        </div>
      }
    </div>
  );
}

export default Welcome;

今度は名前を変更しても現在日時は変わっていません f:id:butorisa:20210919172551g:plain

コンポーネント破棄で実行(Unmounting)

コンポーネントが非表示になったタイミングで実行したい場合は「useEffectの第1引数に実行したい処理を記述」します

import {useState,useEffect} from 'react';
import {useName} from '../logic/useName';

function Welcome() {

  const {disp, changeName} = useName();
  const [show, setShow] = useState(false);
  const [date, setDate] = useState('');

  // useEffectの第1引数return句に記述した処理はコンポーネント破棄時に実行される
  useEffect(() => {
    const now: string = new Date().toLocaleString();
    setDate(now);

    return () => console.log('コンポーネントが破棄されました');
  });

  return (
    <div>
      <p>名前を入力してください</p>
      <input type="text" onBlur={e => changeName(e.target.value)}/>
      <button onClick={() => setShow(true)}>Enter</button>
      {show &&
        <div>
          <p>ようこそ!! {disp}</p>
          <p>{date}</p>
        </div>
      }
    </div>
  );
}

export default Welcome;

コードを修正後ホットリロード時にコンソールに「コンポーネントが破棄されました」と出力されました f:id:butorisa:20210919180412p:plain

第2引数を[]にするとマウント時とアンマウント時に実行されるようになるようです

コンポーネント破棄タイミングってブラウザ閉じた、別サイトへ移動って感じなのかな??

(非表示で破棄になるかと思って画像のbyeボタンを作ったが非表示では破棄されなかった)

参考

useEffectフックのしくみ

Reactの基礎 【ライフサイクル】

【ReactNative】現在の日付、時間表示をする方法

react コンポーネントでstate共有(カスタムフック)

昨日はuseStateを使って入力した名前を画面に表示するところまでできました

今日はstateをロジック処理に移動、入力コンポーネントと表示コンポーネントを分けます

コンポーネント間でstate変数を共有する方法は何種類もありそうでしたが、カスタムフックを使います

(useContextでグローバル変数的に共有する、親コンポーネントを経由して共有するがよく見かけましたが変数のスコープが広がりすぎて面倒なバグを生みそうなのでカスタムフックにしました)

失敗例:入力しても画面表示が切り替わらない

logic/useName.tsx

state変数「name」とnameを変更する「changeName」を返すロジック処理(★カスタムフック)

import React,{useState} from 'react';

export const useName = () => {

  const [name, setName] = useState<string>('risa');

  const changeName = (input:string) => {
    setName(input);
  };

  return {name, changeName};
}

components/InputArea.tsx

nameを変更するテキストボックス

import {useName} from '../logic/useName';

function InputArea() {

  const {changeName} = useName();

  return (
    <input type="text" onBlur={e => changeName(e.target.value)}/>
  );
}

export default InputArea;

components/Header.tsx

nameを表示

import {useName} from '../logic/useName';

function Header() {

  const {name} = useName();

  return (
    <header>
      <p>Hello! {name}</p>
    </header>
  );
}

export default Header;

App.tsx

import Header from './components/Header';
import InputArea from './components/InputArea';

function App() {
  return (
    <div>
      <Header/>
      <InputArea/>
    </div>
  );
}

export default App;

ビルドは成功して画面が表示されますが、フォーカスアウトしても名前が変わらない…

f:id:butorisa:20210918114538g:plain

原因1:nameが描画後にsetNameが実行されている

変更前の状態のnameを表示したままになっていたということです

ロジック処理に表示用のstate変数「disp」を追加してuseEffectでnameが変更されたらdispを変更します

logic/useName.tsx

import React,{useState,useEffect} from 'react';

export const useName = () => {

  const [name, setName] = useState<string>('risa');
  const [disp, setDisp] = useState<string>(name);

  useEffect(() => {
    setDisp(name + 'さん');
  }, [name]);

  const changeName = (input:string) => {
    setName(input);
  };

  return {disp, changeName};
}

原因2:表示コンポーネントがstateの変更を検知できていない

試しに入力コンポーネントに上記でセットされるdispを表示してみます

components/InputArea.tsx

import {useName} from '../logic/useName';

function InputArea() {

  const {disp, changeName} = useName();

  return (
    <div>
      <p>inputエリア:{disp}</p>
      <input type="text" onBlur={e => changeName(e.target.value)}/>
    </div>
  );
}

export default InputArea;

Header.tsxもdispを表示するように修正しましたが、変更されたのはInputArea.tsxの表示のみです!

f:id:butorisa:20210918122928p:plain

changeNameを呼び出したInputArea.tsxコンポーネントには変更後のdispがリターンされますが、Header.tsxは初期表示でしかリターン値をもらえてないんですね

カスタムフックを使ったstateの共有方法

方法1:stateを変更するコンポーネント内で表示も行う

上記「原因2」で修正したInputArea.tsxのように同じコンポーネントでstateの表示と変更を行います

モダンJSのAtomicDesignでコンポーネント再利用する思考からは外れてしまいますが、この方法だとstateを扱う箇所がまとまっているのでコードの見通しも良いです◎

方法2:親コンポーネントでカスタムフックを使いpropsで渡す

どうしてもコンポーネントを分けておきたい場合はpropsで兄弟コンポーネントに渡します

※useName.tsxは上記「原因1」の修正状態です

App.tsx

import Header from './components/Header';
import InputArea from './components/InputArea';
import {useName} from './logic/useName';

function App() {

  const {disp,changeName} = useName();
  const nameProp = {
    disp : disp,
    changeName : changeName
  };

  return (
    <div>
      <Header {...nameProp}/>
      <InputArea  {...nameProp}/>
    </div>
  );
}

export default App;

components/Header.tsx

function Header(nameProp:any) {

  const name = nameProp.disp;

  return (
    <header>
      <p>Hello! {name}</p>
    </header>
  );
}

export default Header;

components/InputArea.tsx

function InputArea(nameProp:any) {

  return (
    <div>
      <p>inputエリア:{nameProp.disp}</p>
      <input type="text" onBlur={e => nameProp.changeName(e.target.value)}/>
    </div>
  );
}

export default InputArea;

f:id:butorisa:20210918135631p:plain

Header.tsxで表示している箇所も名前が変わりましたね!!

練習コードなのでpropsで渡す値も少なくきれいに見えますが、実際のシステムだと扱うコンポーネントと変数が多いのでコードの可読性が下がるかもしれませんね…

私は「方法1:stateを変更するコンポーネント内で表示も行う」が良いかなと思います!

参考

説明をしながらreact+typescriptでカスタムフックを作っていく

【React】「useStateの値を更新しても反映されない!」の解決方法

react useState入門

昨日に引き続きreactHooksの練習をします まずはよく使うuseStateから!!

useStateフック

state変数とsetState関数の定義をするフック(API)です

「useState<型>(初期値)」で使います

const [name, setName] = useState<string>('risa');

<練習>state変数に初期値セットして画面に表示

npx create-react-app プロジェクト名 --template typescript で作成したプロジェクトを修正しています

プロジェクト名/src配下にcomponentsフォルダを作成し、Header.tsxを作成しました

nameをstate変数にして宣言しています {変数名}で画面に表示されます

import React, {useState} from 'react';

function Header() {

  const [name, setName] = useState<string>('risa');

  return (
    <header className="App-header">
      Hello! {name}
    </header>
  );
}

export default Header;

App.tsxを修正して上記のHeader.tsxを読み込むようにします

import React from 'react';
import Header from './components/Header';

function App() {
  return (
    <div className="App">
      <Header/>
    </div>
  );
}

export default App;

yarn start で開発サーバを起動すると表示されました!!

f:id:butorisa:20210917115113p:plain

<練習>入力した名前を画面に表示する

今度はrisaからテキストボックスに入力した名前をフォーカスアウトしたタイミングで表示を切り替えるようにします

components/Header.tsx

import React, {useState} from 'react';

function Header() {

  const [name, setName] = useState<string>('risa');

  function changeName(name:any) {
    setName(name.target.value);
  }

  return (
    <header className="App-header">
      <p>Hello! {name}</p>
      <input type="text" onBlur={e => changeName(e)}/>
    </header>
  );
}

export default Header;

changeName関数を追加してテキストボックス入力値でstate変数を更新しています

フォーカスアウトのイベントはonBlurです

入力した名前で表示を切り替えることができました!

f:id:butorisa:20210917122316g:plain

参考

React入門(TypeScript版)

React再入門<<急がば回れ>>

モダンJSについてほぼ忘れたので今日はReactのそもそもを学び直します

プロジェクト作成

せっかくなのでTypeScriptバージョンの記事にならいました

React × TypeScript 入門

私の環境ではcreate-react-appのバージョンが4.0.3だったので--templateが必要でした!

npx create-react-app {プロジェクト名} --template typescript

この記事にたどり着く前に非TypeScriptのReactプロジェクト作成の記事を読みながらTypeScriptに変えようとしたのですが失敗しました。。。

関数コンポーネント

React:関数コンポーネントとクラスコンポーネントの違い

関数コンポーネントは「コンポーネントの書き方」のこと

以前はクラスコンポーネントというのが主流だったようです(他の言語のクラスと同じですね)

現在は関数コンポーネントが推奨されていて、以下が理由(他にもありますが分かりやすかったものはこちら)

  • おまじないコードがなくなりコードがスッキリする
  • ライフサイクルメソッドからの解放
    • jQuery時代マウント時、描画時…などで同じような処理たくさん書いて読みづらくなったことある
  • stateをコンポーネント間で共有したい時にクラスでは「引数にクラスを取り、新しいクラスを返す」クラスを作成する必要があったが、関数ではuseEffectフックを使ってコンポーネント内にstateが更新された時に実行したい処理を書くことができる

Hooks

関数コンポーネントで利用できるReactが提供しているライブラリまたはAPIのようなもの

  • useState
    • state変数、set関数を定義する
  • useEffect
    • 第2引数のstate変数が更新された時に第1引数のコールバック関数を実行する
  • useRef
    • useRefで定義した変数をテキストボックスref属性にする+イベント関数でstate変数にref変数をセットすることで入力値が表示されるようになる(双方向バインディングのような動き)

Hooksをちゃんと使えるようになると変数の状態管理がきれいにできそう!