昨日は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;
ビルドは成功して画面が表示されますが、フォーカスアウトしても名前が変わらない…
原因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の表示のみです!
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;
Header.tsxで表示している箇所も名前が変わりましたね!!
練習コードなのでpropsで渡す値も少なくきれいに見えますが、実際のシステムだと扱うコンポーネントと変数が多いのでコードの可読性が下がるかもしれませんね…
私は「方法1:stateを変更するコンポーネント内で表示も行う」が良いかなと思います!