こんにちは、ミツモア エンジニア の宮内(@myrsn0104)です。今年もアドベントカレンダーの季節がやってきました。昨年の記事については ミツモアAdvent Calendar 2021 こちらを見てみてください。
この記事では、useStateの正しい使い方について、自身のエピソードを踏まえつつお話ししていきます。(React初心者さん向け)
useStateをめちゃ乱用してた
わたしがReactに初めて触れたのは2年半前、YouTubeで入門者向けのチュートリアルを見た時です
そこで初めて知ったstate管理という概念
これ超便利じゃん👍🏻と思った当時のわたしは、深く考えないままstateを乱用し、React開発をしていました
しかしミツモアにJoinし様々なエンジニアからレビューをもらうなかで、わたしのその開発スタイルは開発効率とFEのパフォーマンスを下げかねない悪行だと気付きました
乱用はなぜ良くないのか
理由は大きく分けて2つあります
- setStateが走るたびに再renderされる = stateの数が多ければ多いほどパフォーマンスが下がる可能性
- コードがごちゃごちゃして読みづらくなる
まず 1 の具体例と回避策について説明します
可能な限り1つのstateにまとめる
以下のようなコンポーネントがあったとします
const FruitForm = () => { const [apple, setApple] = React.useState(0) const [banana, setBanana] = React.useState(0) const [lemon, setLemon] = React.useState(0) const onSubmit = (values) => { setApple(values.apple) setBanana(values.banana) setLemon(values.lemon) } return ( <> <Form onSubmit={onSubmit}> <Input type='number' name='apple' label='りんごの個数' /> <Input type='number' name='banana' label='バナナの個数' /> <Input type='number' name='lemon' label='レモンの個数' /> <Button type='submit' /> </Form> <ResultComponent apple={apple} banana={banana} lemon={lemon} /> </> ) })
それぞれのフルーツの個数を保存するのに、onSubmit時に独立したそれぞれのsetStateを呼び出しています
これだとSubmitボタンを1度クリックするごとに、ResultComponentが3回再レンダリングされてしまいますね
同じタイミングでしか中身が変更されないstateは1つにまとめることが可能なので、以下のように変更します
const FruitForm = () => { const [result, setResult] = React.useState({ apple: 0, banana: 0, lemon: 0, }) const onSubmit = (values) => { const { apple, banana, lemon } = values setResult({ apple, banana, lemon }) } return ( <> <Form onSubmit={onSubmit}> <Input type='number' name='apple' label='りんごの個数' /> <Input type='number' name='banana' label='バナナの個数' /> <Input type='number' name='lemon' label='レモンの個数' /> <Button type='submit' /> </Form> <ResultComponent result={result} /> </> ) })
これで1度の再レンダリングで済むようになりました
では、続いて「2. コードがごちゃごちゃして後で読みづらい」の回避策の例です
useEffect + useState を useMemo に置き換える
useMemo一つで実装できる変数を、useEffectとuseStateどちらも駆使して無駄に長いコードを書いてしまうなんていうのはReactエンジニア初心者さんあるあるではないでしょうか
例えば<ResultComponent />
の中身が以下のようなコンポーネントだったとすると、
const ResultComponent = ({ result }) => { // 表示する文字列を初期値は空文字でセットする const [totalNum, setTotalNum] = React.useState(0) const [text, setText] = React.useStat('') React.useEffect(() => { const num = result.apple + result.banana + result.lemon setTotalNum(num) }, [result]) React.useEffect(() => { if(totalNum < 10) { setText('') return } setText('食べ過ぎです') }, [totalNum]) return <div>{text}</div> })
useMemoを使うとこう置き換えられます
const ResultComponent = ({ result }) => { // resultの値の変化するとuseMemoが文字列を返す const totalNum = React.useMemo(() => result.apple + result.banana + result.lemon,[result]) const text = React.useMemo(() => totalNum < 10 ? '' : '食べ過ぎです',[totalNum]) return <div>{text}</div> })
だいぶコードがスッキリしました
補足)useMemoとuseEffectの違いについて
以下は公式から引っ張ってきました
[useEffect](デフォルトでは副作用関数はレンダーが終了した後に毎回動作しますが、特定の値が変化した時のみ動作させるようにすることもできます。)
副作用を有する可能性のある命令型のコードを受け付けます。
デフォルトでは副作用関数はレンダーが終了した後に毎回動作しますが、特定の値が変化した時のみ動作させるようにすることもできます。
[useMemo](https://ja.reactjs.org/docs/hooks-reference.html#usememo)
メモ化された値を返します。
“作成用” 関数とそれが依存する値の配列を渡してください。
useMemo
は依存配列の要素のいずれかが変化した場合にのみメモ化された値を再計算します。この最適化によりレンダー毎に高価な計算が実行されるのを避けることができます。
useEffectもuseMemoも依存配列の要素が変化した時に処理が走ります
上記のようなuseEffect + useState から useMemoへの置き換えで挙動やパフォーマンスに変化を与えることなくコードの行数を減らし可読性を上げることが可能です
ただし呼び出した子コンポーネントやDOMの中で、変数の値を変更したい場合などにはsetStateを渡す必要があるので、useMemoへの置き換えには向いていません
まとめ
- 同じタイミングでしか中身が変更されないstate達は1つにまとめる
- ユースケースに応じて最適なhooksを利用する
以上、わたしがuseStateの乱用から足を洗った理由と具体例の紹介でした🥸
これを読んで少しでも思い当たる節のあれば、あなたもぜひuseStateの乱用から卒業しましょう
最後に
ミツモアでは様々な職種のエンジニアを積極的に採用しています! ご興味がある方はぜひ気軽に面談しましょう!