本記事は「フロントエンドの開発効率を向上するヒントを教え合おう!」イベントの参加記事です。
Reactで再レンダリングを防止するのはパフォーマンス面で効果があると思いますが、コンポーネントが再レンダリングされる条件、特にmemo化したコンポーネントでのレンダリング条件がいまいちわかってなかったので整理してみました。
再レンダリングのチェック方法は↓の記事に書きましたので参照してください。
親コンポーネントが再レンダリングされたら子コンポーネントも再レンダリングされる
次のような構造の場合、親コンポーネントがステートの変更で再レンダリングされると子コンポーネントも再レンダリングされました。
Parentコンポーネントでinputに何か入力するとonChange
で_count
がインクリメントされるようにしています。
ステートが変わってParentコンポーネントが再レンダリングされるとChildコンポーネントも再レンダリングされます。
また、Childコンポーネントに特にPropsは渡していないのでPropsに依らないことがわかりました。
const Child = () => {
return <div>child</div>;
};
const Parent = () => {
const [_count, setCount] = useState(0);
const onChange = useCallback(() => {
setCount((prev) => prev + 1);
}, [setCount]);
return (
<div>
<input onChange={onChange} />
<Child />
</div>
);
};
子コンポーネントをmemo化してみる
子コンポーネントにPropsを渡さない場合
この場合はChildコンポーネントはmemo化でキャッシュされるので再レンダリングされませんでした。
const Child = memo(() => {
return <div>child</div>;
});
子コンポーネントにPropsを渡す場合
Propsがprimitiveな値で構成される場合
※ primitiveな値はここではnumberとかstringを指すことにします。
ParentコンポーネントからPropsに同じ値を渡してみました。
この場合もChildコンポーネントは再レンダリングされませんでした。
const Child = memo((props: { num: number }) => {
return <div>child</div>;
});
const Parent = () => {
... // 省略
return (
<div>
<input onChange={onChange} />
<Child num={1} />
</div>
);
Propsを構成する値にオブジェクトが含まれる場合
オブジェクトをChildコンポーネント呼び出し時に定義する場合
Propsにオブジェクトが含まれる場合ですが、Childコンポーネントを呼び出す部分にベタ書きでオブジェクトを書いて渡してみました。
この場合はChildコンポーネントは再レンダリングされました。
値が変わらなくてもオブジェクト自体はParentコンポーネントがレンダリングされる度に生成されるので、React側では別オブジェクトとして判定されてしまいます。
const Child = memo((props: { obj: { num: number }}) => {
return <div>child</div>;
});
const Parent = () => {
... // 省略
return (
<div>
<input onChange={onChange} />
<Child obj={{num: 1}} />
</div>
);
オブジェクトをChildコンポーネント呼び出し前に定義する場合
次にChildコンポーネントを呼び出す前にParentコンポーネント内に定数として宣言して渡してみました。
この場合もChildコンポーネントは再レンダリングされました。
先ほどと同様、React側で別オブジェクトとして判定されてしまっています。
const Child = memo((props: { obj: { num: number }}) => {
return <div>child</div>;
});
const Parent = () => {
... // 省略
const obj = {num: 1};
return (
<div>
<input onChange={onChange} />
<Child obj={obj} />
</div>
);
オブジェクトをuseMemoで定義する場合
次にオブジェクトをuseMemoで定義して渡してみました。
この場合はChildコンポーネントは再レンダリングされませんでした。
先ほどとは違い、React側でオブジェクトが変わっていないという判定をされているようです。
const Child = memo((props: { obj: { num: number }}) => {
return <div>child</div>;
});
const Parent = () => {
... // 省略
const obj = useMemo(() => ({ num: 1 }), []);
return (
<div>
<input onChange={onChange} />
<Child obj={obj} />
</div>
);
オブジェクトをuseStateで定義する場合
最後によく使うだろうパターンとして、オブジェクトをuseStateで定義して渡してみました。
この場合もChildコンポーネントは再レンダリングされませんでした。
const Child = memo((props: { obj: { num: number }}) => {
return <div>child</div>;
});
const Parent = () => {
... // 省略
const [obj] = useState({ num: 1 });
return (
<div>
<input onChange={onChange} />
<Child obj={obj} />
</div>
);
React.memoのProps比較
React.memoが再レンダリングする条件については公式のドキュメントに書かれています。
React.memo は props の変更のみをチェックします。React.memo でラップしているあなたのコンポーネントがその実装内で useState、useReducer や useContext フックを使っている場合、state やコンテクストの変化に応じた再レンダーは発生します。
Propsのオブジェクトについては
デフォルトでは props オブジェクト内の複雑なオブジェクトは浅い比較のみが行われます。比較を制御したい場合は 2 番目の引数でカスタム比較関数を指定できます。
というように書かれていて、該当部分のコードを見るとオブジェクトはis関数で===
で比較されていました。
なのでオブジェクト内の値が一致しているかどうかではなく、そもそもオブジェクトが一致しないといけないということになります。
まとめ
- 通常何もしなければ、親コンポーネントが再レンダリングされると子コンポーネントは再レンダリングされる。
- 子コンポーネントをmemo化して再レンダリングを防ごうとしても、Propsにオブジェクトが渡されている場合はHooks(useMemo、useState、useCallbackなど)で状態管理しないと再レンダリングされてしまう。
再レンダリングを防ぐことが必須ではないとは思いますが、この辺を意識すると思わぬ再レンダリングを防ぐことはできそうです!