Reactキャッチアップへの道② 『レンダリングの最適化』
こんにちは!
スマレジ テックファームのMichiです!
前週に引き続き、Reactのキャッチアップをしていきます。今回は、『レンダリングの最適化』について学びます。
コンポーネントが再レンダリングされる条件
前回のおさらいです。Reactにおいて、コンポーネントの再レンダリングが発生する条件は、(大まかに言って)以下の三つでした。
- コンポーネント内の状態管理されている値(
state
)が変更されたとき - 親コンポーネントからプロパティ(
props
)を受け取っている場合、そのプロパティ値が変更されたとき - 親コンポーネントが再レンダリングされたとき
今回はその中でも、3. 親コンポーネントが再レンダリングされたとき
の挙動について見ていきます。
サンプルコード
次のようなサンプルコードを用意します。
ひとつめのParent.jsx
は、外部からの入力値を受け付けるフォームと、子コンポーネントChild.jsx
の表示切り替えるボタンを持ちます。
ふたつめのChild.jsx
は、Parent.jsx
から受け取ったprops
の値を使用して、コンポーネントの中身を表示するかどうかを決定しています。
Parent.jsx
import { useState } from "react"; import { Child } from "./Child"; export const Parent = () => { const [text, setText] = useState(""); const [isShow, setIsShow] = useState(false); const onChangeText = (e) => setText(e.target.value); const onClickShow = () => setIsShow(!isShow); return ( <div> <input value={text} onChange={onChangeText} /> <button onClick={onClickShow}>表示</button> <Child isShow={isShow} /> </div> ); }
Child.jsx
export const Child = (props) => { const { isShow } = props; console.log("Childがレンダリングされました"); return ( <> {isShow ? ( <div> <p>子コンポーネント</p> </div> ) : null} </> ); };
画面からフォームに値を入力するたびに、Child.jsx
が再レンダリングされ、コンソールに「Childがレンダリングされました」のメッセージが表示されます。 これは、親のParent.jsx
内で、text
の値に変更が生じたために再レンダリングが発生し、その影響で子のChild.jsx
も同時に再レンダリングされてしまうためです。
今の状況は、Child.jsx
内の値は何も更新されていないにもかかわらず、再レンダリングが走っています。これは不要なレンダリングなので、起こさないようにしたいです。
memo - コンポーネントのメモ化 -
親から渡されたprops
の値に変更が生じたとき以外、コンポーネントの再レンダリングを起こさないようにする機能がmemo
です。
使い方は簡単で、Reactからmemo
をインポートし、適用したいコンポーネントをラップするだけです。
Child.jsx
import {memo} from "react" // 適用したいコンポーネントをmemoでラップする export const Child = memo((props) => { const { isShow } = props; console.log("Childがレンダリングされました"); return ( <> {isShow ? ( <div> <p>子コンポーネント</p> </div> ) : null} </> ); });
先ほどは、フォームに値を入力するたびにコンソールにメッセージが表示されていましたが、
今回は最初に画面を読み込んだタイミング、もしくは、『表示』ボタンをクリックしてprops
の値が変更された時にしか、メッセージが表示されていません。親コンポーネントが再レンダリングされても、子コンポーネントは再レンダリングされていないことが分かります。
このように、ある状態を記憶(キャッシュ)して、不要な再計算をスキップさせることを、Reactでは「メモ化する」と言ったりします。
useCallBack - 関数のメモ化 -
先ほどのmemo
では、コンポーネント全体をメモ化しました。これに対して、特定の関数のみをメモ化する機能がuseCallback
です。
memo
の解説で使用したサンプルコードに機能を追加します。Child.jsx
内に『閉じる』ボタンを設置し、クリックするとChild.jsx
の内容が非表示になるようにします。
Parent.jsx
import { useState } from "react"; import { Child } from "./Child"; export const Parent = () => { const [text, setText] = useState(""); const [isShow, setIsShow] = useState(false); const onChangeText = (e) => setText(e.target.value); const onClickShow = () => setIsShow(!isShow); // 『閉じる』機能用の関数 const onClickClose = () => setIsShow(false) return ( <div> <input value={text} onChange={onChangeText} /> <button onClick={onClickShow}>表示</button> {/* onClickCloseをpropsで渡す */} <Child isShow={isShow} onClickClose={onClickClose}/> </div> ); }
Child.jsx
import {memo} from "react" export const Child = memo((props) => { const { isShow, onClickClose } = props; console.log("Childがレンダリングされました"); return ( <> {isShow ? ( <div> <p>子コンポーネント</p> {/* 閉じるボタンを追加する */} <button onClick={onClickClose}>閉じる</button> </div> ) : null} </> ); });
Child.jsx
の表示を決定するフラグisShow
は親コンポーネント側にあるので、フラグの値をfalse
にするための関数onClickClose
をParent.jsx
内で定義し、props
で関数ごと子コンポーネントに渡します。
するとどうでしょう。再び、フォームに値を入力するたびに再レンダリングが走るようになりました。(Child.jsx
はメモ化しているのにもかかわらずです!)
実は、関数宣言においては、中身や返り値の値に変化がなくとも、再レンダリングが走るたびに、毎回新しい関数を生成する仕組みになっています。
新しい関数onClickClose
が生成されたので、props
の値も更新されているとReactが判断し、子コンポーネントにも再レンダリングの処理が走るわけです。このような場合は、useCallback
を使用し、関数のメモ化を行う必要があります。
使い方はmemo
のときと同じく、ReactからuseCallback
をインポートし、適用したい関数をラップします。memo
と違うのは、第二引数に依存配列を取るところです。今回は、関数onClickClose
は初期値から変更されることはないので、依存配列の中身は空([]
)で設定します。
Parent.jsx
import { useState, useCallback } from "react"; import { Child } from "./Child"; export const Parent = () => { const [text, setText] = useState(""); const [isShow, setIsShow] = useState(false); const onChangeText = (e) => setText(e.target.value); const onClickShow = () => setIsShow(!isShow); // 閉じる機能の関数をメモ化する const onClickClose = useCallback(() => setIsShow(false), []); return ( <div> <input value={text} onChange={onChangeText} /> <button onClick={onClickShow}>表示</button> {/* onClickCloseをpropsで渡す */} <Child isShow={isShow} onClickClose={onClickClose} /> </div> ); };
これで、フォームに値を入力したときでも、再び再レンダリングが走らないようになりました。
useMemo - 変数のメモ化 -
最後に、変数のメモ化についても軽く触れておきます。
変数をメモ化するには、useMemo
を使用します。
使い方はuseCallback
とまったく同じです。例えば、次のように定義することで、初期レンダリング時のみ1 + 2
の計算を行い、再レンダリング時にはキャッシュ値を使用する(再計算を行わない)ようにすることができます。
const sample = useMemo(() => 1 + 2, []);
正直、この程度の計算では、パフォーマンスに違いはほぼ出ません。もっと複雑で大きい計算が必要な時に、useMemo
が効力を発揮します。
まとめ
Reactキャッチアップの2週目は、Reactのレンダリング最適化についてのアウトプットでした。
Vue.jsに慣れた自分としては、わざわざ手動で最適化しなければならないのが面倒くさいなと思ったりするのですが、そこがマスターできればReact初心者卒業なんでしょうね(^_^;)