buto > /dev/null

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

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の値を更新しても反映されない!」の解決方法