Reactのコンポーネントが再レンダリングされるのは以下の3つの場合。
- stateが更新された時(同じ値をセットした場合は、Reactが最適化して再レンダリングをスキップします)
- propsが変更された時(React.memo()でメモ化されている子コンポーネントはpropsが変わらない限り再描画されません)
- 親コンポーネントが再レンダリングされた時(再レンダリングされたコンポーネント配下の子要素は再レンダリング)
→対策React.memo()
(子コンポーネントが props 変化しない限りスキップ)useMemo()
やuseCallback()
で関数・値の再生成を防止
コンポーネントをkey
に依存して動的に切り替えない設計
(React18で開発環境でかつStrictModeのとき、2回レンダリングが走る。)
memo()
memo()の使い方はコンポーネントをmemo()で囲みます。
そうすることでpropsが変更されない限り再レンダリングしないで済みます。
import { memo } from "react"
export const ChildBox = memo((props) => { return() })
あるいはこう書きます↓
// React.memo() を使う場合
const ChildBox = React.memo((props) => {
return <div>{props.name}</div>;
});
ただし、オブジェクトや配列は参照が変わるとレンダリングされます。
const [user, setUser] = useState({ name: 'John' });
setUser({ name: 'John' }); // 見た目は同じでも新しいオブジェクト → 再レンダリングされる
javascritptで↓こうなるのは、2つのオブジェクトは同じ内容でも異なるメモリ上の参照(アドレス)を持っているから。
{ name: 'John' } !== { name: 'John' }
const a = { name: 'John' };
const b = { name: 'John' };
console.log(a === b); // false ❌
===は中身ではなくアドレスを比較するので、falseになります。
const obj = { name: 'John' };
const a = obj;
const b = obj;
console.log(a === b); // true ✅
↑この場合はaもbも同じメモ帳の住所を持っているので、trueになります。
一方で文字列、数値、booleanなどのプリミティブ型は値を直接比較します。
const x = 'John';
const y = 'John';
console.log(x === y); // true ✅
オブジェクトの中身を比較するときはJSON.stringify()が使えます。
const a = { name: 'John' };
const b = { name: 'John' };
console.log(JSON.stringify(a) === JSON.stringify(b)); // true ✅
話がそれましたが、
const Parent = () => {
const [user, setUser] = useState({ name: 'John' });
return (
<>
<div>{user.name}</div>
<button onClick={() => setUser({ name: 'John' })}>更新</button>
</>
);
};
↑これは押すたびにレンダリングされるので、
このように対処します。↓
const [user, setUser] = useState({ name: 'John' });
const handleClick = () => {
// オブジェクトの参照が変更されないようにする
if (user.name !== 'John') {
setUser({ name: 'John' });
}
};
配列も同様に毎回新しい配列として見なされ、再レンダリングしてしまいます。
import React from "react";
const Parent = () => {
const items = [1, 2, 3]; // 毎回新しい配列
return <Child list={items} />;
};
const Child = React.memo(({ list }) => {
console.log("Child rendered");
return <ul>{list.map(item => <li key={item}>{item}</li>)}</ul>;
});
export default Parent;
useCallback()
そこで、memo化しても再レンダリングされるのを防ぐのがuseCallback()です。
useCallback()は処理が変わらない場合は同じ関数として処理し、
メモ化されたコールバック関数を返すために使用され、
子コンポーネントに渡される関数が不要に再生成されるのを防ぎます。
useCallback()は、第2引数に配列を設定できます。
第2引数に依存関係の配列を設定し、この配列の要素が変わらない限り、
コールバック関数は再生成されません。
配列を空にすれば、最初に設定したものをずっと使うという処理になります。
import { useCallback } from "react"
const onClickClose = useCallback(() => setOpen(false) , []);
useMemo()
useMemo()は値(変数や計算結果)をメモ化するために使用します。
第二引数は依存配列を設定でき、その配列の値が変更されたときにのみ計算が再実行されます。
import React, { useState, useMemo } from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
const expensiveCalculation = (num) => {
console.log('Calculating...');
return num * 2;
};
// useMemoを使って計算結果をメモ化
const result = useMemo(() => expensiveCalculation(count), [count]);
return (
<div>
<p>Result: {result}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
export default Parent;
key
Reactでのkeyはコンポーネントや要素を再描画するための識別子です。
特に.map()で、リストを描画するときにどの要素が変わったのかをReactが正確に把握するために必要です。
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
keyにインデックスを使うのは非推奨です。