メモ化
React.memo, useCallback, useMemoに共通する目的はメモ化である。
メモ化とは広範な言い方をするとキャッシュのこと。
React.memo
ラップした関数コンポーネントの初回レンダー結果を記憶し、次回以降同じpropsが与えられた時に、記憶した結果を使用することで、無駄なレンダーをスキップできパフォーマンスを向上させることができる。
React.memoを使用しない場合
const CounterItem = ({ count, label }) => {
console.log(`${label}がレンダリング`);
return <div>{label}: {count}</div>;
};
const CounterList = () => {
const [count1, setCount1] = useState(0);
const increment1 = () => setCount1(c => c + 1);
const [count2, setCount2] = useState(0);
const increment2 = () => setCount2(c => c + 1);
return (
<>
<button onClick={increment1}><CounterItem count={count1} label="左" /></button>
<button onClick={increment2}><CounterItem count={count2} label="右" /></button>
</>
);
};
左右ボタンのどっちを押しても、CounterListコンポーネント内のstateが更新されるので左右のCounterItemコンポーネントを両方再レンダーしてしまう。左ボタンを押した場合は数字が変更される左のCounterItemコンポーネントのみレンダーしたい。右のコンポーネントは変更されてないのでレンダーの必要がない。
React.memoを使用する場合
// React.memoで関数コンポーネントをラップ
const CounterItem = React.memo(({ count, label }) => {
console.log(`${label}がレンダリング`);
return <div>{label}: {count}</div>;
});
const CounterList = () => {
const [count1, setCount1] = useState(0);
const increment1 = () => setCount1(c => c + 1);
const [count2, setCount2] = useState(0);
const increment2 = () => setCount2(c => c + 1);
return (
<>
<button onClick={increment1}><CounterItem count={count1} label="左" /></button>
<button onClick={increment2}><CounterItem count={count2} label="右" /></button>
</>
);
};
propsが変更されたコンポーネントしかレンダーされない。
useCallback
第一引数に渡したコールバック関数をメモ化する。第二引数に渡した値の配列が変更された場合のみ、コールバック関数が作り直される。React.memoと併用することでパフォーマンスを向上させることができる。
useCallbackを使用しない場合
const CounterItem = React.memo(({ count, label, onClick }) => {
console.log(`${label}がレンダリング`);
return <button onClick={onClick}><div>{label}: {count}</div></button>;
});
const CounterList = () => {
const [count1, setCount1] = useState(0);
const increment1 = () => setCount1(c => c + 1);
const [count2, setCount2] = useState(0);
const increment2 = () => setCount2(c => c + 1);
return (
<>
<CounterItem count={count1} label="左" onClick={increment1} />
<CounterItem count={count2} label="右" onClick={increment2} />
</>
);
};
左ボタンを押した場合、左右のCounterItemコンポーネントを両方再レンダーしてしまう。CounterItemコンポーネントをReact.memoでラップしてるのでpropsの変更がないコンポーネント(右ボタン)はレンダーをスキップして欲しいができていない。
上記の問題は参照の同一性が関係している。
"a" === "a" // true
1 === 1 // true
const a = () => {}
const b = () => {}
// 同じ処理であってもfalseが返る
a === b //false
stateの更新によりCounterListコンポーネントの再レンダリングが起きると、CounterListコンポーネント内のincrement1関数や increment2関数が再度定義される。再定義された関数は上記で示したように同じ参照ではなくなる。つまりCounterItemコンポーネントに渡す関数が同じ参照にはならない。よってReact.memoでラップしてもporpsの参照が違うのでレンダーがスキップされない。
useCallbackを使用する場合
useCallbackを使用することで上記の問題が解決される
const CounterItem = React.memo(({ count, label, onClick }) => {
console.log(`${label}がレンダリング`);
return <button onClick={onClick}><div>{label}: {count}</div></button>;
});
const CounterList = () => {
const [count1, setCount1] = useState(0);
const increment1 = useCallback(() => setCount1(c => c + 1), []);
const [count2, setCount2] = useState(0);
const increment2 = useCallback(() => setCount2(c => c + 1), []);
return (
<>
<CounterItem count={count1} label="左" onClick={increment1} />
<CounterItem count={count2} label="右" onClick={increment2} />
</>
);
};
うまくメモ化できていることが確認できる。
useMemo
useCallbackはコールバック関数をメモ化したが、useMemoは値をメモ化する。
useMemoのユースケースは2つある。
useMemoのユースケース1
オブジェクトをコンポーネントのpropsとして渡す前に、オブジェクトをメモ化する
useMemoを使用しない場合
const CounterItem = React.memo(({ params }) => {
console.log(`${params.label}がレンダリング`);
return <button onClick={params.handler}><div>{params.label}: {params.value}</div></button>;
});
const CounterList = () => {
const [count1, setCount1] = useState(0);
const increment2 = useCallback(() => setCount2(c => c + 1), []);
const [count2, setCount2] = useState(0);
const increment1 = useCallback(() => setCount1(c => c + 1), []);
const params1 = {
label: '左',
value: count1,
handler: increment1
}
const params2 = {
label: '右',
value: count2,
handler: increment2
}
return (
<>
<CounterItem params={params1} />
<CounterItem params={params2} />
</>
);
};
上記のコードの場合も片方のボタンを押すと両方のボタンが再レンダリングされており、うまくメモ化できていない。左ボタンを押した際にはparams1のcount1が変更されるので左ボタンは再レンダリングされるべきだが、右ボタンの再レンダリングはスキップしたい。
上記の問題も参照の同一性が関係している。オブジェクトの中身が同じでも同じ参照にはならない。
{'a': 1} === {'a': 1} // false
[1,2,3] === [1,2,3] // false
stateの更新によりCounterListコンポーネントの再レンダリングが起きると、params1とparams2が再度定義される。つまりCounterItemコンポーネントに渡すparamsが同じ参照にはならないため、例えuseCallbackでコールバック関数をメモ化しても、最終的に渡されるpropsの参照が違うのでレンダリングがスキップされない。
useMemoを使用する場合
上記の問題をuseMemoを使用することで解決できる。
const CounterItem = React.memo(({ params }) => {
console.log(`${params.label}がレンダリング`);
return <button onClick={params.handler}><div>{params.label}: {params.value}</div></button>;
});
const CounterList = () => {
const [count1, setCount1] = useState(0);
const increment2 = useCallback(() => setCount2(c => c + 1), []);
const [count2, setCount2] = useState(0);
const increment1 = useCallback(() => setCount1(c => c + 1), []);
const params1 = useMemo(() => ({
label: '左',
value: count1,
handler: increment1
}), [count1, increment1])
const params2 = useMemo(() => ({
label: '右',
value: count2,
handler: increment2
}), [count2, increment2])
return (
<>
<CounterItem params={params1} />
<CounterItem params={params2} />
</>
);
};
うまくメモ化できているのが確認できる。
useMemoのユースケース2
重い計算結果をメモ化する。
useMemoを使用しない場合
// 何かしらの重い計算
const heavyCalc = (count) => {
let i = 0
while (i < 2000000000) i++
return count
}
const Counter = () => {
const [count1, setCount1] = useState(0);
const increment1 = () => setCount1(c => c + 1);
const [count2, setCount2] = useState(0);
const increment2 = () => setCount2(c => c + 1);
const result = heavyCalc(count1)
return (
<>
<button onClick={increment1}><div>重い計算あり: {result}</div></button>
<button onClick={increment2}><div>重い計算なし: {count2}</div></button>
</>
)
};
どちらのボタンを押しても、コンポーネント内の重い計算処理のせいで再レンダリングが遅れる。重い計算があるheavyCalc関数に渡しているのはcount1だけなので、右ボタンを押した場合はすぐ再レンダリングして欲しい。
useMemoを使用する場合
上記の問題をuseMemoを使用することで解決できる。
// 何かしらの重い計算
const heavyCalc = (count) => {
let i = 0
console.log('重い計算開始')
while (i < 2000000000) i++
return count
}
const Counter = () => {
const [count1, setCount1] = useState(0);
const increment1 = () => setCount1(c => c + 1);
const [count2, setCount2] = useState(0);
const increment2 = () => setCount2(c => c + 1);
const result = useMemo(() => heavyCalc(count1), [count1])
return (
<>
<button onClick={increment1}><div>重い計算あり: {result}</div></button>
<button onClick={increment2}><div>重い計算なし: {count2}</div></button>
</>
)
};
左ボタンを押した時は重い計算処理が走るが、右ボタンを押した場合はメモ化した値を返すため無駄な重い計算処理は走っていないので再レンダリングが早い。