はじめに
初めまして、新卒1年目のm_you_sanと申します。
初学者向けにReactにおけるメモ化の方法を簡単に紹介させていただきます。
目次は以下の通りです。
そもそもメモ化って?
メモ化は簡単に言うと、計算結果を保持して、それを再利用する手法です。
Reactの場合、無駄なコンポーネントの呼び出しを減らすことができ、パフォーマンス性が上がります。
特にレンダリングの負荷が大きい処理、頻繫に再レンダリングされるコンポーネントの子コンポーネントで、メモ化を利用するとよりパフォーマンス性の向上が見込めます。
メモ化の方法
Reactおけるメモ化方法は主に3つ(React.memo、useCallBack、useMemo)あります。
今回はReact.memoとuseCallBackについて紹介いたします。
React.memo
React.memoはコンポーネントの不要なレンダリングの回避を目的として使用します。
メモ化の対象はコンポーネントです。
React.memoはpropsが等価であるかをチェックして再レンダリングの判断をします。
新しく渡されたpropsと前回のpropsを比較して、等価である場合は再レンダリングは起きません。
使用例
以下のコードは親コンポーネントに入力フォームがあるコードです。
親コンポーネント
const App: React.FC = () => { const items = ["食洗器", "髭剃り", "冷蔵庫"]; const [itemList, setItemList] = useState(items); const [item, setItem] = useState(""); const addItem = () => { setItemList([...itemList, item]); } return ( <> <input type="text" placeholder="欲しいもの" value={item} onChange={(e) => setItem(e.target.value)} /> <button onClick={addItem}>追加</button> <List itemList={itemList} /> </> ) }
type ListProps = { itemList: string[] } export const List: React.FC<ListProps> = ({itemList}) => { console.log("レンダリングされました"); return ( <ul> {itemList.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ) }
この場合、フォームに値が入力される度に、親コンポーネントで管理しているitemが更新されるので、子コンポーネントがレンダリングされてしまいます。
こうしたレンダリングを防ぐために、子コンポーネントをメモ化します。
下記のコードの場合、itemListが更新されたときのみ、再レンダリングが起こるようになります。
つまり、追加ボタンが押されない限り、再レンダリングは起きません。
子コンポーネント
export const List: React.FC<ListProps> = React.memo(({itemList}) => { console.log("レンダリングされました"); return ( <ul> {itemList.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ) })
動作を確認すると、メモ化することで、再レンダリングが防げていることが分かります。
また、追加ボタンを押したときのみ、再レンダリングが起きているのが分かります。
注意点
関数をpropsとして渡す場合、React.memoでは再レンダリングを防ぐことができません。
例として、先程のコードに、欲しいものの表示、非表示を切り替えるtoggleShowItems関数を作成し、propsとして子コンポーネントに渡してみます。
親コンポーネント
const App: React.FC = () => { const items = ["食洗器", "髭剃り", "冷蔵庫"] const [itemList, setItemList] = useState(items); const [item, setItem] = useState(""); const [isShow, setIsShow] = useState(false); const addItem = () => { setItemList([...itemList, item]); } const toggleShowItems = () => { setIsShow(!isShow); } return ( <> <input type="text" placeholder="欲しいもの" value={item} onChange={(e) => setItem(e.target.value)} /> <button onClick={addItem}>追加</button> <List itemList={itemList} isShow={isShow} toggleShowItems={toggleShowItems} /> </> ) }
type ListProps = { itemList: string[]; isShow: boolean; toggleShowItems: () => void; } export const List: React.FC<ListProps> = React.memo(({itemList, isShow, toggleShowItems}) => { console.log("レンダリングされました"); return ( <> <div> <button onClick={toggleShowItems}> {isShow? "欲しいものを非表示にする" : "欲しいものを表示する"} </button> </div> {isShow && ( <ul> {itemList.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> )} </> ) })
この場合、toggleShowItems関数の内容は変更されていないにも関わらず、フォームに入力する度に再レンダリングが発生してしまいます。
なぜこのようになるのかというと、React.memoでは、関数などのオブジェクト型の等価性をチェックする場合、値そのものではなく参照先データを比較するからです。
仮にtoggleShowItems1をレンダリング前の関数、toggleShowItems2をレンダリング後の関数とします。
関数の内容自体は変わりませんが、参照先がレンダリング前後で変わるため、等価ではないと判断されます。
そのため関数が変更されたと見なされ、再レンダリングが発生してしまいます。
//レンダリング前 const toggleShowItems1 = () => { setIsShow(!isShow); } //レンダリング後 const toggleShowItems2 = () => { setIsShow(!isShow); } //関数の内容は同じだが、参照先が異なるためfalse console.log(toggleShowItems1 === toggleShowItems2);
こうした問題を解決するために使用するのがuseCallbackです。
useCallback
useCallbackのメモ化の対象は関数です。
useEffectなどと同様に、第二引数に依存配列を設定します。
下記のコードの場合、isShowが更新されたときに、関数を再生成します。
親コンポーネント
const toggleShowItems = useCallback(() => { setIsShow(!isShow); }, [isShow]);
動作を確認すると、フォームに入力してもレンダリングが発生していないことが分かります。
また、表示切替のボタンを押して、isShowが更新されたタイミングでレンダリングが起きていることが分かります。
このように、関数をメモ化したコンポーネントに渡す場合、useCallbackを使用することで、不要なレンダリングを抑えることができます。
注意点
useCallbackをReact.memoと併用して、不要なレンダリングを防ぐ目的ではなく、単純に関数の再生成を防ぐ目的で使用しても、あまり効果はありません。
理由としては、関数の再生成のコストがuseCallbackの実行コストを上回るケースがあまりないからです。
関数の再生成を防ぐ目的であれば、useMemoの方が適していると思うので、そちらを使用するのが良いと思います。
まとめ
今回は初学者向けにReact.memoとuseCallbackについて、紹介させていただきました。
メモ化について更に詳しく知りたい方は、公式ドキュメントを参照していただければと思います。