概要
- 再レンダーは「stateの更新時(set関数)」「親の再レンダー時」に行われる
- 再レンダー時にはそのコンポーネントの持つ変数や関数が再生成される
- メモ化は再レンダーや再生成を制御する
- メモ化は計算した内容をキャッシュする
- 再レンダー/再生成の前にObject.is()を用いて依存配列の値を検証し、すべて同じなら制御する
- コンポーネントのメモ化にはReact.memoを用いて、親の再レンダー時に再レンダーを制御する
- 変数や関数のメモ化にはuseCallback・useMemoを用いて再生成を制御する
再レンダーについて
再レンダーのチェック用コードは以下
import { useState } from "react";
export default function Parent() {
const [count, setCount] = useState<number>(0);
console.log("親のレンダー時");
return (
<div>
<Child />
<button onClick={() => setCount((prev) => ++prev)}>parent:{count}</button>
</div>
);
}
function Child() {
const [childCount, setChildCount] = useState<number>(0);
console.log("子のレンダー時");
return (
<button onClick={() => setChildCount((prev) => ++prev)}>
child:{childCount}
</button>
);
}
レンダーが行われるタイミングはReact 公式ドキュメントから
1.コンポーネントの初回レンダー。
2.コンポーネント(またはその祖先のいずれか)の state の更新。
1について、初回レンダー時は以下のようにParent -> Childの順番にレンダーが行われています。
2について、childボタンをクリックするとChildのstateが更新されるため、Childが再レンダーされます。
次に、parentボタンをクリックするとParentのstateが更新されてParentが再レンダーされます。加えて、Childも同様に再レンダーが行われるのを確認できます。
この挙動は公式ドキュメント(前述)の「ステップ 2:React がコンポーネントをレンダー」で説明されています。
更新されたコンポーネントが他のコンポーネントを返す場合、次にそのコンポーネントを React がレンダーし、そのコンポーネントも何かコンポーネントを返す場合、そのコンポーネントも次にレンダーし、といった具合に続きます。このプロセスは、ネストされたコンポーネントがなくなり、React が画面に表示されるべき内容を知り尽くすまで続きます。
今回の場合だと「Parentが再レンダーされた際に、Parentが保持しているコンポーネントであるChildも再レンダーされる」という挙動になっています。
再生成
サンプルコードは以下
import { useEffect, useState } from "react";
export default function Parent() {
const [count, setCount] = useState<number>(0);
const [stateObj] = useState<{ name: string }>({ name: "name" });
const onClick = () => setCount((prev) => ++prev);
const one = 1;
const obj = { name: "name" };
return (
<div>
<Child
onClick={onClick}
count={count}
one={one}
obj={obj}
stateObj={stateObj}
/>
</div>
);
}
function Child({
onClick,
count,
one,
obj,
stateObj,
}: {
onClick: () => void;
count: number;
one: number;
obj: { name: string };
stateObj: { name: string };
}) {
useEffect(() => console.log("初回のみ"), []);
useEffect(() => console.log("関数"), [onClick]);
useEffect(() => console.log("count"), [count]);
useEffect(() => console.log("定数"), [one]);
useEffect(() => console.log("object"), [obj]);
useEffect(() => console.log("stateのobject"), [stateObj]);
return <button onClick={onClick}>count:{count}</button>;
}
再レンダーが行われると同時に、変数やオブジェクトの再生成が行われます(メモリから消されて、もっかい作るみたいな認識)
useStateで管理している場合はReactが状態管理しているためset関数を呼び出さない限りは再生成されません。
再生成されているかの判別にはuseEffectを用います。
useEffectは初回時と、依存配列(useEffectの後ろの[])の中のいずれかの値が変化した場合に呼び出されます。
「変化した」という状態はプリミティブ型の場合(stringやnumber)は値の直接的な比較、オブジェクト(関数)の場合は参照先のメモリが一致するかで判別されます(Object.is()が利用されます)。
サンプルコードでは、初回起動時はすべてのuseEffectが走ります。
ボタンを押すと子コンポーネントが再レンダーされて、関数、count、objectについて走っているのが確認できます。
よって、この3つの値が変化していると判別されていることになります。
各値について、
1.初回のみ:依存配列が空なので、初回以外は走らない
2.関数:再生成されて参照先が変わったので再レンダー時に走る
3.count: 0 -> 1に変化しているため再レンダー時に走る
4.定数: 再生成されているが、値が変化していないので走らない
5.object :2と同様に参照が変わっているので再レンダー時に走る
6.stateのobject:useStateで管理しているため参照が変化せず走らない
メモ化
再レンダーや再生成を制御するにはメモ化を行うことで制御が可能です。
コンポーネントのメモ化
レンダーの方ではParentのボタンを押した際に再レンダーされ、Childも再レンダーされていました。
しかし、ChildはParentの内部に配置してるだけなのでParentが再レンダーされる場合にChildが再レンダーされる必要はないです。
ここで、コンポーネントをメモ化すると親が再レンダーされた場合に子が再レンダーされるのを制御することができます。
const Child = memo(function Child() {
const [childCount, setChildCount] = useState<number>(0);
console.log("子のレンダー時");
return (
<button onClick={() => setChildCount((prev) => ++prev)}>
child:{childCount}
</button>
);
});
このようにmemoで囲うとメモ化できます。
この状態でparentボタンをクリックするとParentだけ再レンダーされます。
メモ化している場合は親の再レンダー時に再レンダーされなくなりますが、親から受け取るparamが変化した場合には再レンダーが引き起こされます。
以下のようにcountをparamに追加するとmemo化以前と同じようにparentボタンを押すとChildも再レンダーされるようになります。
import { memo, useState } from "react";
export default function Parent() {
const [count, setCount] = useState<number>(0);
console.log("親のレンダー時");
return (
<div>
<Child count={count} />
<button onClick={() => setCount((prev) => ++prev)}>parent:{count}</button>
</div>
);
}
const Child = memo(function Child({ count }: { count: number }) {
const [childCount, setChildCount] = useState<number>(0);
console.log("子のレンダー時");
return (
<button onClick={() => setChildCount((prev) => ++prev)}>
child:{childCount}
</button>
);
});
関数と変数のメモ化
関数オブジェクトや変数をメモ化すると再生成を制御できます。
関数のメモ化にはuseCallback,それ以外のメモ化にはuseMemoを用います。
メモ化する際にはuseEffectと同様に依存配列を設定します。依存配列の値が変更されない限りは再生成が行われません。
import { useCallback, useEffect, useMemo, useState } from "react";
export default function Parent() {
const [count, setCount] = useState<number>(0);
const onClick = useCallback(() => setCount((prev) => ++prev), []);
const obj = useMemo(() => {
return { name: "name" };
}, []);
return (
<div>
<Child onClick={onClick} count={count} obj={obj} />
</div>
);
}
function Child({
onClick,
count,
obj,
}: {
onClick: () => void;
count: number;
obj: { name: string };
}) {
useEffect(() => console.log("関数"), [onClick]);
useEffect(() => console.log("count"), [count]);
useEffect(() => console.log("object"), [obj]);
return <button onClick={onClick}>count:{count}</button>;
}
ボタンを押すとメモ化した関数とオブジェクトは再生成されなくなっているのが確認できます。
メモ化のメリット
- 不要な再レンダーを防げる
- 同じデータを表示し続けるだけのコンポーネント
- 操作していないコンポーネント
- 不要な再生成を防げる
- 常に同じ値である変数/関数
- 特定の場合にのみ変化するUIのstyleの計算
- memo化してるコンポーネントのparamにオブジェクトを渡す場合
メモ化の注意点
- メモリにキャッシュを作成する
- Object.is()で比較が毎回行われる
- 常に再レンダー/再生成されるような場合には意味はない
メモ化のオーバーヘッドについて
こちらの記事で検証してくださっています。