React.memoを使ったレンダリング最適化入門
はじめに
React のメモ化をなんとなく使う現状から卒業したかったので、下記リンクを参考に[1]React.memo
useCallback
useMemo
をそれぞれ実装してみたまとめ
記事のターゲット
-
React.memo
useCallback
useMemo
をどこか遠ざけていた人 - 親や子コンポーネントのレンダリングタイミングが気になる人
-
レンダリングの最適化
がかっこいい響きに見える人 - React 書き始めた人に
そのコンポーネントちゃんと最適化してる?
とかドヤりたい人
環境
関係ありそうなとこだけ
"dependencies": {
"next": "10.0.3",
"react": "17.0.1",
"react-dom": "17.0.1",
},
実際に挙動を確認するために実装したコードはここに
どのタイミングでコンポーネントはレンダリングされるのか
自分の state が更新されたときに自分自身+使っている子コンポーネントが再レンダリングされる
type ChildProps = {
count: number;
};
const Child = ({ count }: ChildProps): JSX.Element => {
useEffect(() => {
console.log("Childがレンダリングされたよ");
});
return <p>Child:{count}</p>;
};
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0);
const [childCount, setChildCount] = useState<number>(0);
useEffect(() => {
console.log("Parentがレンダリングされたよ");
});
return (
<div>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1);
}}
>
Parent count up
</button>
<button
type="button"
onClick={() => {
setChildCount1(childCount1 + 1);
}}
>
Child1 count up
</button>
<p>Parent:{parentCount}</p>
<Child count={childCount} />
</div>
);
};
この場合setParentCount
とsetChildCount
が呼ばれ state の値が更新されるたびにChild
もParent
も再レンダリングされる
これぐらいの要素数なら特段気にする必要もないが、もしこの Child コンポーネントがレンダリングされるたびに重い計算処理をしていたり、Child コンポーネントのなかに大量に表示される要素がある場合、レンダリングコストが高くなる
Child コンポーネントをメモ化しておけば Child が描画するのに関係ない値が親コンポーネント側で変更されてもレンダリングをスキップすることができる
メモ化してみる
React.memo
を使うと親から子コンポーネントに渡している props が更新されない限り(後述する callback 関数とかは別)子コンポーネントは再レンダリングされない
- import { useEffect, useState } from 'react'
+ import { memo, useEffect, useState } from 'react'
+ const ChildMemo = memo<ChildProps>(({ count }) => <Child count={count} />)
return (
<div>
- <Child count={childCount} />
+ <ChildMemo count={childCount} />
</div>
)
この場合setParentCount
が呼ばれてもメモ化されたChild
は再レンダリングされずにParent
だけ再レンダリングされる(Child に渡している props の値は更新されていないので)
setChildCount
でChild
の props に渡している値を更新するとChild
もParent
も再レンダリングされる
useMemo と React.memo の違い
React.memo の hook 版じゃねと思ったら違ったって話。
さっきのコードを useMemo で書き換えてみる
- import { memo, useEffect, useState } from 'react'
+ import { useEffect, useMemo, useState } from 'react'
- const ChildMemo = memo<ChildProps>(({ count }) => <Child count={count} />)
+ const childMemo = useMemo(() => <Child count={childCount} />, [])
return (
<div>
- <ChildMemo count={childCount} />
+ {childMemo}
</div>
)
useMemo
は第二引数に依存配列を指定する
依存配列で指定した値が更新された時に再レンダリングされる
今回の書き方だと依存配列に何も指定していないのでChild
はレンダリングされない
依存配列を指定することでchildCount
が更新されたらChild
が再レンダリングされる様になる
- const childMemo = useMemo(() => <Child count={childCount} />, [])
+ const childMemo = useMemo(() => <Child count={childCount} />, [childCount])
React.memo 化された子コンポーネントの props に callback 関数を渡したときの挙動
親で定義した関数を子コンポーネントに props として渡すと子コンポーネントを memo 化しても再レンダリングされる
Child コンポーネント
type ChildProps = {
count: number
+ handleClick: () => void
}
- return <p>Child:{count}</p>;
+ return (
+ <>
+ <p>Child:{count}</p>
+ <button
+ type="button"
+ onClick={() => {
+ handleClick()
+ }}
+ >
+ Child Button
+ </button>
+ </>
+ )
Parent コンポーネント
+ const handleClick = () => {
+ console.log('Childのボタンが押されたよ')
+ }
return (
<div>
{/* ....省略 */}
- <ChildMemo count={childCount} />
+ <ChildMemo count={childCount} handleClick={handleClick} />
</div>
)
親がレンダリングされるたびに親で定義した関数も再生成されるので結果として、子コンポーネントを memo 化しても親の state が更新されると子コンポーネントも再レンダリングされてしまう
useCallback
親がレンダリングされるたびに親で定義した関数も再生成されないようにuseCallback
で関数をメモ化する
useCallback は useMemo の糖衣構文っぽい
useCallback(fn, deps) は useMemo(() => fn, deps) と等価です。
https://ja.reactjs.org/docs/hooks-reference.html#usecallback
- import { memo, useCallback, useEffect, useState } from 'react'
+ import { memo, useEffect, useState } from 'react'
- const handleClick = () => {
- console.log('Childのボタンが押されたよ')
- }
+ const handleClick = useCallback(() => {
+ console.log('Childのボタンが押されたよ')
+ }, [])
useCallback
はuseMemo
と同様(useMemo の糖衣構文っぽいしそりゃそうか)、第二引数に依存配列を受け取り、依存配列で指定した値が更新されると useCallback でメモ化した関数が再計算される
結果として、親の state が更新されても memo 化された子コンポーネントは再レンダリングされない
useCallback する関数に引数がある場合どうすればいいの?
単に useCallback で渡す関数を引数付きにしてあげれば良い
Child コンポーネント
type ChildProps = {
count: number
- handleClick: () => void
+ handleClick: (text: string) => void
}
<button
type="button"
onClick={() => {
- handleClick()
+ handleClick('Childのボタンが押されたよ')
}}
>
Parent
- const handleClick = useCallback(() => {
+ const handleClick = useCallback((text: string) => {
- console.log('Childのボタンが押されたよ')
+ console.log(text)
}, [])
userReducer と memo 化を組み合わせたときの挙動
reducer の実装って immutable な気がしていて、state がオブジェクトの場合、一部のプロパティを更新しただけでも他のコンポーネントも再レンダリングされるんじゃ・・と思ってたらそうでなかったってお話
reducer 周り
export type State = {
count1: number;
count2: number;
count3: number;
count4: number;
};
export const updateCount1 = (
state: State,
{ value }: UpdateCount1Payload
): State => ({ ...state, count1: value });
// ... updateCount2, updateCount3, updateCount4が続く
export enum ActionType {
UpdateCount1 = "UpdateCount1",
// ...
}
export type Action =
| {
type: ActionType.UpdateCount1;
payload: UpdateCount1Payload;
}
| {
type: ActionType.UpdateCount2;
payload: UpdateCount2Payload;
};
// ...
export const reducer: Reducer<State, Action> = (state, action) => {
switch (action.type) {
case ActionType.UpdateCount1:
return updateCount1(state, action.payload);
// ...
default:
throw new TypeError(`unexpected action. ${action}`);
}
};
子コンポーネント
const Child = ({ count, label }: ChildProps): JSX.Element => {
useEffect(() => {
console.log(`Child${label}がレンダリングされたよ`);
});
return (
<p>
Child{label}:{count}
</p>
);
};
const ChildMemo = memo<ChildProps>(({ count, label }) => {
return <Child count={count} label={label} />;
});
親コンポーネント
import { memo, useCallback, useEffect, useReducer, useState } from "react";
import { ActionType } from "state/action";
import { reducer } from "state/reducer";
import { initialState } from "state";
const Parent: NextPage = () => {
const [parentCount, setParentCount] = useState<number>(0);
const [childState, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
console.log("Parentがレンダリングされたよ");
});
return (
<div>
<button
type="button"
onClick={() => {
setParentCount(parentCount + 1);
}}
>
Parent count up
</button>
<button
type="button"
onClick={() => {
dispatch({
type: ActionType.UpdateCount1,
payload: {
value: childState.count1 + 1,
},
});
}}
>
Child1 count up
</button>
{/* count2, count3...の値をカウントアップするようのボタンが同様にある */}
<p>Parent:{parentCount}</p>
<ChildMemo count={childState.count1} label="01" />
{/* count2, count3...の値をcountのpropに渡すChildMemoコンポーネントがある */}
</div>
);
};
Child
コンポーネントをメモ化したChildMemo
は reducer で定義された state のプロパティを props として受け取っているので、setParentCount
が呼ばれて親の state が更新されてもChild
コンポーネントは再レンダリングされない
reducer で定義された state のプロパティを更新した場合は、そのプロパティを props として渡しているChild
コンポーネントとParent
コンポーネントのみ再レンダリングされる
まとめ
敬遠してたメモ化だけども、手を動かして実装してみると意外とイメージできたからヨキでした
-
記載されているコードから気になる部分を深ぼって実装したので似た様なコードがあります。とてもわかりやすい記事だったのでそちらもご参考に。 ↩︎
Discussion