useMemoについてとメモ化の意味
公開日:
タグ:
- #Next.js
- #React
useMemoとは何か?
useMemoは「値(配列やオブジェクト)の参照を安定させるフック」です。
依存配列の値が変わらない限り、前回と同じ参照の値を返します。
useMemoを使って配列をメモ化する
ITEMS という配列を検索してフィルターをかける機能を例に useMemoについて説明していきたいと思います。
はまずは useMemo を使わない状態です。
'use client';
import { memo, useState } from 'react';
type Item = {
id: number;
name: string;
price: number;
};
const ITEMS: Item[] = [
{ id: 1, name: 'Apple', price: 300 },
{ id: 2, name: 'Orange', price: 250 },
// ...
];
export default function Page() {
const [count, setCount] = useState(0);
const [keyword, setKeyword] = useState('');
const filteredItemsWithoutMemo = (() => {
console.log('Filtering items... (without useMemo)');
return ITEMS.filter((item) =>
item.name.toLowerCase().includes(keyword.toLowerCase())
);
})();
return (
<div>
<Counter setCount={setCount} />
<div>Count: {count}</div>
<h1>Items Filter</h1>
<label>
Filter by name:
<input
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
</label>
<ItemList items={filteredItemsWithoutMemo} />
</div>
);
}
const Counter = ({
setCount,
}: {
setCount: React.Dispatch<React.SetStateAction<number>>;
}) => <button onClick={() => setCount((c) => c + 1)}>Increment</button>;
const ItemList = memo(function ItemList({ items }: { items: Item[] }) {
console.log('ItemList Rendered');
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name} - ${item.price}
</li>
))}
</ul>
);
});
ItemList.displayName = 'ItemList';
上記のコードでは、 count の値を更新するたびに filteredItemsWithoutMemo が生成され、参照が変わるため、 ItemList コンポーネントも再レンダリングされています。
原因は filter が毎回「新しい配列」を返し、 items の参照が変わるためです。
React.memo は props の参照を比較しているため、配列の中身が同じでも再レンダリングが発生します。
そこで、filter の結果を useMemo でメモ化します。
ItemList の再レンダリングを防ぐには配列を生成している処理を useMemo でメモ化します。 第二引数には、コールバック内で参照する値(state/props/変数など)を並べます。
const filteredItemsWithMemo = useMemo(() => {
console.log('Filtering items... (with useMemo)');
return ITEMS.filter((item) =>
item.name.toLowerCase().includes(keyword.toLowerCase())
);
}, [keyword]);
こうすることで、親コンポーネントが再レンダリングしても、ItemList の再レンダリングを防ぐことができます。
useMemoを使っているパターンと使っていないパターンを比較できるコードはこちらです。
'use client';
import { memo, useMemo, useState } from 'react';
type Item = {
id: number;
name: string;
price: number;
};
const ITEMS: Item[] = [
{ id: 1, name: 'Apple', price: 300 },
{ id: 2, name: 'Orange', price: 250 },
// ...
];
export default function Page() {
const [count, setCount] = useState(0);
const [keyword, setKeyword] = useState('');
const [useMemoEnabled, setUseMemoEnabled] = useState(true);
const filteredItemsWithMemo = useMemo(() => {
console.log('Filtering items... (with useMemo)');
return ITEMS.filter((item) =>
item.name.toLowerCase().includes(keyword.toLowerCase())
);
}, [keyword]);
const filteredItems = useMemoEnabled
? filteredItemsWithMemo
: (() => {
console.log('Filtering items... (without useMemo)');
return ITEMS.filter((item) =>
item.name.toLowerCase().includes(keyword.toLowerCase())
);
})();
return (
<div>
<label htmlFor="useMemoEnabled">
<input
type="checkbox"
id="useMemoEnabled"
name="useMemoEnabled"
checked={useMemoEnabled}
onChange={(e) => setUseMemoEnabled(e.target.checked)}
/>
useMemo を使う
</label>
<Counter setCount={setCount} />
<div>Count: {count}</div>
<h1>Items Filter</h1>
<label>
Filter by name:
<input
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
</label>
<ItemList items={filteredItems} />
</div>
);
}
const Counter = ({
setCount,
}: {
setCount: React.Dispatch<React.SetStateAction<number>>;
}) => <button onClick={() => setCount((c) => c + 1)}>Increment</button>;
const ItemList = memo(function ItemList({ items }: { items: Item[] }) {
console.log('ItemList Rendered');
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name} - ${item.price}
</li>
))}
</ul>
);
});
ItemList.displayName = 'ItemList';
useMemoEnabled が true の時は filteredItems がメモ化された状態、 false の時はメモ化されていない状態をテストできます。
useMemoが必要な時
1. 生成した配列 / オブジェクトを props で渡していて、子を React.memo している時(参照を固定したい)
const filteredItems = ITEMS.filter(/* ... */);
return <ItemList items={filteredItems} />; // ← 毎回新しい配列参照
filter / map / sortは毎回新しい配列を返す- 中身が同じでも参照が変わる
React.memo(ItemList)が効かず、子が再レンダリングされる
const filteredItems = useMemo(() => {
return ITEMS.filter(/* ... */);
}, [keyword]);
keywordが変わらない限り同じ参照を返す- 親の無関係な state 更新(countなど)で子の再レンダリングを抑えられる
2. filter / sort / reduce などの計算が重く、再計算を減らしたい時(計算コストを下げたい)
React.memo と関係なく、計算自体が重いなら useMemo 単体でも意味があります。
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);
- 親が再レンダリングしても
itemsが変わらないなら再計算しない - 件数が多い / 計算が高コストな場合に効きやすい
※ただし「軽い計算」なら後述の通り不要になりがちです。
3. 子に渡す “設定オブジェクト” を安定させたい時(オプション props の参照固定)
const options = { threshold: 0.5 }; // 毎回新しい参照
useSomeHook(options);
↓
const options = useMemo(() => ({ threshold: 0.5 }), []);
useSomeHook(options);
• 参照が変わると、内部で再初期化・再購読が走る系で効きます
useMemoが不要な時
1. そもそも新しい配列/オブジェクトを作っていない(参照が変わらない)時
return <ItemList items={ITEMS} />;
• props の参照が安定しているなら useMemo は不要
2. 子コンポーネントが React.memo されていない時(参照固定の恩恵が出にくい)
- 子コンポーネントが
React.memoされていない時
親が再レンダリングすると子も基本再レンダリングされるので参照を固定して再レンダリングを止めるという目的で使用する場合は効果が見えづらいです
※ただし計算が重いので再計算を減らしたいという目的なら、子が memo されていなくても意味はあります。
3. 計算が軽い / 配列が小さい時(管理コストの方が勝ちやすい)
const filtered = items.filter(/* 数件 */);
- useMemoのために依存配列を管理する
- 読みやすさが落ちる
このコストの方が大きい場合が多いです。
まとめ
filter 、 map 、 sortなどは毎回新しい配列を返すため、propsとして渡すと参照が変わります。そういった場面では useMemo が効果を発揮します。また、useMemo は依存が変わらない限り同じ参照を返すので、無関係なstate更新で子の再レンダリングを抑えることができました。
またこれまでReactのメモ化について学んできたことで、全てに共通しているのは、「子コンポーネントに渡すpropsの参照が変わるかどうか」を基準に、使うべきかどうかを判断できるという点でした。
どのメモ化も「参照が変わるかどうか」を軸に考えると、使いどころが判断しやすいと感じました。
memo→ コンポーネントuseCallback→ 関数useMemo→ 値
どれもむやみに利用することは保守性を損なうため避けたいですが、パフォーマンスの改善につながる利用であれば積極的に取り入れていきたいです。