useMemo
は、再レンダリング間の計算結果をキャッシュできるReactフックです。
const cachedValue = useMemo(calculateValue, dependencies)
参照
useMemo(calculateValue, dependencies)
コンポーネントの最上位レベルでuseMemo
を呼び出して、再レンダリング間の計算をキャッシュします。
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
// ...
}
パラメータ
-
calculateValue
: キャッシュする値を計算する関数。ピュア関数でなければならず、引数を取らず、任意の型の値を返す必要があります。Reactは最初のレンダリング時にこの関数を呼び出します。次のレンダリングでは、dependencies
が前回のレンダリング以降に変更されていない場合、同じ値を再び返します。それ以外の場合は、calculateValue
を呼び出し、その結果を返し、後で再利用できるように保存します。 -
dependencies
:calculateValue
コード内で参照されるすべてのリアクティブ値のリスト。リアクティブ値には、props、state、コンポーネント本体内で直接宣言されたすべての変数と関数が含まれます。リンターがReact用に設定されている場合、すべてのリアクティブ値が依存関係として正しく指定されていることを確認します。依存関係のリストは、一定数の項目を持ち、[dep1, dep2, dep3]
のようにインラインで記述する必要があります。Reactは、Object.is
比較を使用して、各依存関係とその前の値を比較します。
戻り値
最初のレンダリングでは、useMemo
は引数なしでcalculateValue
を呼び出した結果を返します。
次のレンダリングでは、前回のレンダリングからの既に保存されている値を返すか(依存関係が変更されていない場合)、calculateValue
を再度呼び出して、calculateValue
が返した結果を返します。
注意事項
useMemo
はHookなので、コンポーネントの最上位レベル、または独自のHook内でのみ呼び出すことができます。ループや条件分岐内では呼び出すことができません。そのような必要がある場合は、新しいコンポーネントを作成して状態をそこに移動してください。- Strict Modeでは、Reactは偶発的な不純物を検出するのを助けるために、計算関数を2回呼び出します。これは開発時のみの動作であり、本番環境には影響しません。計算関数が純粋関数である場合(そうあるべきですが)、これはロジックに影響を与えません。呼び出しのいずれかからの結果は無視されます。
- Reactはキャッシュされた値を、特別な理由がない限り破棄しません。例えば、開発中は、コンポーネントのファイルを編集すると、Reactはキャッシュを破棄します。開発時と本番時において、初期マウント中にコンポーネントが中断した場合、Reactはキャッシュを破棄します。将来、Reactはキャッシュの破棄を利用する機能を追加する可能性があります。例えば、将来的にReactが仮想リストの組み込みサポートを追加する場合、仮想化されたテーブルのビューポートからスクロールアウトしたアイテムのキャッシュを破棄することが理にかなっています。
useMemo
をパフォーマンス最適化のみに依存する場合は問題ありません。そうでない場合は、状態変数またはrefの方が適切な場合があります。
使用方法
高価な再計算のスキップ
再レンダリング間の計算をキャッシュするには、コンポーネントの最上位レベルでuseMemo
呼び出しでラップします。
import { useMemo } from 'react';
function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}
useMemo
には2つの引数を渡す必要があります。
- 引数を取らず、
() =>
のように、計算結果を返す計算関数です。 - 計算で使用されるコンポーネント内のすべての値を含む依存関係のリストです。
最初のレンダリングでは、useMemo
から取得する値は、計算関数を呼び出した結果になります。
以降のレンダリングでは、Reactは依存関係を前回のレンダリング時に渡した依存関係と比較します。依存関係のいずれも変更されていない場合(Object.is
で比較)、useMemo
は既に計算した値を返します。そうでない場合、Reactは計算を再実行し、新しい値を返します。
つまり、useMemo
は、依存関係が変更されるまで、再レンダリング間の計算結果をキャッシュします。
これがいつ役立つのかを例を通して見ていきましょう。
デフォルトでは、Reactは再レンダリングするたびにコンポーネントの本体全体を再実行します。TodoList
が状態を更新するか、親から新しいpropsを受け取ると、filterTodos
関数が再実行されます。
function TodoList({ todos, tab, theme }) {
const visibleTodos = filterTodos(todos, tab);
// ...
}
通常、これは問題になりません。ほとんどの計算は非常に高速だからです。しかし、大規模な配列をフィルタリングまたは変換したり、高価な計算を実行したりする場合、データが変更されていない場合は再実行をスキップしたいと思うかもしれません。todos
とtab
の両方が前回のレンダリング時と同じである場合、先に示したように計算をuseMemo
でラップすると、既に計算済みのvisibleTodos
を再利用できます。
このタイプのキャッシングは、メモ化と呼ばれます。
詳細
一般的に、数千個のオブジェクトを作成したり、ループ処理を行ったりしない限り、コストはそれほど高くありません。より確信を得たい場合は、コンソールログを追加してコードの実行時間を測定できます。
console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');
測定する操作(たとえば、入力への文字入力)を実行します。すると、コンソールにfilter array: 0.15ms
のようなログが表示されます。ログに記録された合計時間が大幅な時間(たとえば1ms
以上)に達する場合は、その計算をメモ化することが理にかなっているかもしれません。実験として、useMemo
で計算をラップして、その操作のログに記録された合計時間が減少したかどうかを確認できます。
console.time('filter array');
const visibleTodos = useMemo(() => {
return filterTodos(todos, tab); // Skipped if todos and tab haven't changed
}, [todos, tab]);
console.timeEnd('filter array');
useMemo
は、最初のレンダリングを高速化しません。更新時の不要な作業をスキップするのに役立ちます。
ご自身のマシンはユーザーのマシンよりも高速である可能性が高いので、人工的な遅延を伴ってパフォーマンスをテストすることをお勧めします。たとえば、Chromeには、この目的でCPUスロットリングオプションがあります。
また、開発環境でのパフォーマンス測定では、最も正確な結果が得られないことに注意してください。(たとえば、StrictModeが有効になっている場合、各コンポーネントは1回ではなく2回レンダリングされます。)最も正確なタイミングを得るには、アプリケーションを本番環境向けにビルドし、ユーザーと同じデバイスでテストしてください。
詳細
アプリケーションがこのサイトのようなものであり、ほとんどの操作が粗粒度(ページ全体またはセクション全体の置き換えなど)である場合、メモ化は通常不要です。一方、アプリケーションが図面エディターのようなものであり、ほとんどの操作が微細粒度(図形の移動など)である場合、メモ化は非常に役立つ場合があります。
useMemo
による最適化は、少数のケースでのみ価値があります。
useMemo
に入れている計算が著しく遅く、その依存関係がめったに変更されない場合。memo
でラップされたコンポーネントにプロップとして渡します。値が変更されていない場合は、再レンダリングをスキップしたいと考えています。メモ化により、依存関係が同じでない場合にのみコンポーネントが再レンダリングされます。- 渡している値は後で、いくつかのフックの依存関係として使用されます。たとえば、別の
useMemo
計算値がそれに依存している可能性があります。あるいは、useEffect
からこの値に依存している可能性もあります。
他のケースでは、計算をuseMemo
でラップすることには利点がありません。それを行うことにも大きな害はありません。そのため、一部のチームは個々のケースを考えずに、できるだけ多くのメモ化を行うことを選択しています。このアプローチの欠点は、コードの可読性が低下することです。また、すべてのメモ化が効果的であるとは限りません。「常に新しい」単一の値があれば、コンポーネント全体のメモ化が中断される可能性があります。
実際には、いくつかの原則に従うことで、多くのメモ化を不要にすることができます。
- コンポーネントが視覚的に他のコンポーネントをラップする場合、JSXを子として受け入れるようにします。これにより、ラッパーコンポーネントが独自のステートを更新すると、Reactはその子コンポーネントを再レンダリングする必要がないことを認識します。
- ローカルステートを優先し、必要以上にステートを上に持ち上げるのは避けてください。たとえば、フォームやアイテムがホバーされているかどうかなどの一時的なステートは、ツリーの一番上またはグローバルステートライブラリに保持しないでください。
- レンダリングロジックをピュアに保つ。コンポーネントの再レンダリングが問題を引き起こしたり、目に見えるアーティファクトを作成したりする場合は、コンポーネントのバグです!メモ化を追加する代わりに、バグを修正してください。
- ステートを更新する不要なエフェクトを避ける。Reactアプリケーションのパフォーマンスの問題のほとんどは、コンポーネントを何度もレンダリングさせるエフェクトから発生する更新の連鎖によって引き起こされます。
- エフェクトから不要な依存関係を削除するようにしてください。たとえば、メモ化の代わりに、オブジェクトまたは関数をエフェクト内またはコンポーネントの外に移動する方が簡単な場合が多いです。
特定の操作がまだ遅いと感じる場合は、React Developer Toolsプロファイラーを使用して、どのコンポーネントがメモ化によって最も恩恵を受けるかを調べ、必要に応じてメモ化を追加します。これらの原則は、コンポーネントのデバッグと理解を容易にするため、いずれの場合でも従うのが良いでしょう。長期的に見ると、微細粒度のメモ化を自動的に行うことで、この問題を完全に解決するための研究を行っています。
例 1の 2: useMemoによる再計算のスキップ
この例では、filterTodos
の実装は人工的に遅延されています。そのため、レンダリング中に呼び出しているJavaScript関数が実際に遅い場合に何が起こるかを確認できます。タブを切り替え、テーマを切り替えてみてください。
タブの切り替えは遅いように感じます。これは、遅延したfilterTodos
を強制的に再実行するためです。tab
が変更されたため、計算全体を再実行する必要があります。(それが2回実行される理由が気になる場合は、こちらで説明されています。)
テーマを切り替えます。useMemoのおかげで、人工的な遅延にもかかわらず高速です!todos
とtab
の両方(useMemo
への依存関係として渡すもの)が前回のレンダリング以降変更されていないため、遅いfilterTodos
呼び出しはスキップされました。
import { useMemo } from 'react'; import { filterTodos } from './utils.js' export default function TodoList({ todos, theme, tab }) { const visibleTodos = useMemo( () => filterTodos(todos, tab), [todos, tab] ); return ( <div className={theme}> <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p> <ul> {visibleTodos.map(todo => ( <li key={todo.id}> {todo.completed ? <s>{todo.text}</s> : todo.text } </li> ))} </ul> </div> ); }
コンポーネントの再レンダリングスキップ
場合によっては、useMemo
を使用すると、子コンポーネントのレンダリングのパフォーマンスを最適化することもできます。例として、このTodoList
コンポーネントがvisibleTodos
をプロップとして子List
コンポーネントに渡すとします。
export default function TodoList({ todos, tab, theme }) {
// ...
return (
<div className={theme}>
<List items={visibleTodos} />
</div>
);
}
theme
プロップを切り替えるとアプリが一瞬フリーズすることに気づいたと思いますが、<List />
をJSXから削除すると高速になります。これはList
コンポーネントの最適化を試みる価値があることを示しています。
デフォルトでは、コンポーネントが再レンダリングされると、Reactはすべての子を再帰的に再レンダリングします。そのため、TodoList
が異なるtheme
で再レンダリングされると、List
コンポーネントも再レンダリングされます。これは、再レンダリングに多くの計算を必要としないコンポーネントには問題ありません。しかし、再レンダリングが遅いことを確認した場合は、memo
でラップすることで、プロップが前回のレンダリングと同じ場合にList
に再レンダリングをスキップするように指示できます。
import { memo } from 'react';
const List = memo(function List({ items }) {
// ...
});
この変更により、List
は、そのすべてのプロップが前回のレンダリングと同じである場合に再レンダリングをスキップします。ここでキャッシュされた計算が重要になります!useMemo
なしでvisibleTodos
を計算した場合を考えてみてください。
export default function TodoList({ todos, tab, theme }) {
// Every time the theme changes, this will be a different array...
const visibleTodos = filterTodos(todos, tab);
return (
<div className={theme}>
{/* ... so List's props will never be the same, and it will re-render every time */}
<List items={visibleTodos} />
</div>
);
}
上記の例では、filterTodos
関数は常に異なる配列を作成します。これは{}
オブジェクトリテラルが常に新しいオブジェクトを作成する方法に似ています。通常、これは問題ではありませんが、List
のプロップは決して同じにならず、memo
の最適化が機能しません。ここでuseMemo
が役立ちます。
export default function TodoList({ todos, tab, theme }) {
// Tell React to cache your calculation between re-renders...
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // ...so as long as these dependencies don't change...
);
return (
<div className={theme}>
{/* ...List will receive the same props and can skip re-rendering */}
<List items={visibleTodos} />
</div>
);
}
visibleTodos
の計算をuseMemo
でラップすることにより、再レンダリング間で同じ値を持つようにします(依存関係が変わるまで)。 特定の理由がない限り、useMemo
で計算をラップする必要はありません。この例では、memo
でラップされたコンポーネントに渡すためであり、これにより再レンダリングをスキップできます。useMemo
を追加する他のいくつかの理由があり、このページでさらに説明されています。
詳細
memo
でList
をラップする代わりに、<List />
JSXノード自体をuseMemo
でラップできます。
export default function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
return (
<div className={theme}>
{children}
</div>
);
}
動作は同じです。visibleTodos
が変更されていない場合、List
は再レンダリングされません。
<List items={visibleTodos} />
のようなJSXノードは{ type: List, props: { items: visibleTodos } }
のようなオブジェクトです。このオブジェクトの作成は非常に安価ですが、Reactはその内容が前回と同じかどうかを知りません。そのため、デフォルトでは、ReactはList
コンポーネントを再レンダリングします。
ただし、Reactが前回のレンダリング時と同じJSXを検出した場合、コンポーネントの再レンダリングは試みません。これは、JSXノードが不変であるためです。JSXノードオブジェクトは時間の経過とともに変化することはないため、Reactは再レンダリングをスキップしても安全であることを認識しています。ただし、これが機能するには、ノードが実際に同じオブジェクトである必要があり、コード上では単に同じに見えるだけでは不十分です。これが、この例でuseMemo
が行うことです。
JSXノードを手動でuseMemo
にラップするのは便利ではありません。たとえば、これを条件付きで行うことはできません。通常、これがJSXノードをラップするのではなく、コンポーネントをmemo
でラップする理由です。
例 1の 2: useMemo
とmemo
を使った再レンダリングのスキップ
この例では、List
コンポーネントは人為的に速度を落とされています。これにより、レンダリングしているReactコンポーネントが実際に遅い場合に何が起こるかを確認できます。タブを切り替えたり、テーマを切り替えたりしてみてください。
タブの切り替えが遅いのは、速度を落としたList
の再レンダリングを強制するためです。tab
が変更されたため、ユーザーの新しい選択を画面に反映する必要があるため、これは予想される動作です。
次に、テーマの切り替えを試みてください。useMemo
とmemo
のおかげで、人為的な速度低下にもかかわらず、高速です! List
は、visibleTodos
配列が前回のレンダリング以降変更されていないため、再レンダリングをスキップしました。visibleTodos
配列が変更されていないのは、todos
とtab
(useMemo
への依存関係として渡す)の両方が前回のレンダリング以降変更されていないためです。
import { useMemo } from 'react'; import List from './List.js'; import { filterTodos } from './utils.js' export default function TodoList({ todos, theme, tab }) { const visibleTodos = useMemo( () => filterTodos(todos, tab), [todos, tab] ); return ( <div className={theme}> <p><b>Note: <code>List</code> is artificially slowed down!</b></p> <List items={visibleTodos} /> </div> ); }
Effectの発火頻度の抑制
場合によっては、Effect内で値を使用したい場合があります。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = {
serverUrl: 'https://localhost:1234',
roomId: roomId
}
useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...
これにより問題が発生します。すべてのリアクティブな値は、Effectの依存関係として宣言する必要があります。 しかし、options
を依存関係として宣言すると、Effectがチャットルームに継続的に再接続される原因になります。
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // 🔴 Problem: This dependency changes on every render
// ...
これを解決するには、Effectから呼び出す必要があるオブジェクトをuseMemo
でラップできます。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = useMemo(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Only changes when roomId changes
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Only changes when createOptions changes
// ...
これにより、useMemo
がキャッシュされたオブジェクトを返す場合、options
オブジェクトは再レンダリング間で同じになります。
しかし、useMemo
はパフォーマンスの最適化であり、セマンティックな保証ではないため、特定の理由がある場合、Reactはキャッシュされた値を破棄することがあります。これにより、Effectも再発火するため、関数依存関係の必要性をなくすことがさらに優れています。オブジェクトをEffectの内部に移動することで実現できます。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
const options = { // ✅ No need for useMemo or object dependencies!
serverUrl: 'https://localhost:1234',
roomId: roomId
}
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Only changes when roomId changes
// ...
これで、コードはシンプルになり、useMemo
は必要なくなりました。Effectの依存関係の削除について詳しく学ぶ。
別のフックの依存関係のメモ化
コンポーネント本体で直接作成されたオブジェクトに依存する計算があるとします。
function Dropdown({ allItems, text }) {
const searchOptions = { matchMode: 'whole-word', text };
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body
// ...
このようなオブジェクトに依存すると、メモ化の目的が損なわれます。コンポーネントが再レンダリングされると、コンポーネント本体内のすべてのコードが再び実行されます。searchOptions
オブジェクトを作成するコード行も、毎回再実行されます。 searchOptions
はuseMemo
呼び出しの依存関係であり、毎回異なるため、Reactは依存関係が異なることを認識し、searchItems
を毎回再計算します。
これを修正するには、依存関係として渡す前にsearchOptions
オブジェクト自体をメモ化できます。
function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text };
}, [text]); // ✅ Only changes when text changes
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // ✅ Only changes when allItems or searchOptions changes
// ...
上記の例では、text
が変更されなかった場合、searchOptions
オブジェクトも変更されません。ただし、さらに優れた修正方法は、searchOptions
オブジェクトの宣言をuseMemo
計算関数の内部に移動することです。
function Dropdown({ allItems, text }) {
const visibleItems = useMemo(() => {
const searchOptions = { matchMode: 'whole-word', text };
return searchItems(allItems, searchOptions);
}, [allItems, text]); // ✅ Only changes when allItems or text changes
// ...
これで、計算はtext
に直接依存するようになりました(これは文字列であり、「誤って」異なるようになることはありません)。
関数のメモ化
memo
でラップされたForm
コンポーネントがあるとします。プロップとして関数を渡したいと考えています。
export default function ProductPage({ productId, referrer }) {
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}
return <Form onSubmit={handleSubmit} />;
}
{}
が異なるオブジェクトを作成するのと同じように、function() {}
や() => {}
のような関数宣言は、毎回異なる関数を生成します。それ自体では、新しい関数を作成することは問題ではありません。これは避けるべきことではありません!ただし、Form
コンポーネントがメモ化されている場合、おそらくプロップが変更されていない場合は再レンダリングをスキップしたいと考えています。常に異なるプロップは、メモ化の目的を無効にします。
useMemo
で関数をメモ化するには、計算関数が別の関数を返す必要があります。
export default function Page({ productId, referrer }) {
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
};
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
これはぎこちなく見えます!関数のメモ化はReactにそのための組み込みフックがあるほど一般的です。関数をuseCallback
でラップし、追加のネストされた関数を作成する必要性を回避してください。useMemo
の代わりに
export default function Page({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
上記の2つの例は完全に同等です。useCallback
の唯一の利点は、追加のネストされた関数の記述を回避できることです。それ以外は何も行いません。useCallback
の詳細を読む。
トラブルシューティング
私の計算は、再レンダリングごとに2回実行されます
厳格モードでは、Reactは一部の関数を1回ではなく2回呼び出します。
function TodoList({ todos, tab }) {
// This component function will run twice for every render.
const visibleTodos = useMemo(() => {
// This calculation will run twice if any of the dependencies change.
return filterTodos(todos, tab);
}, [todos, tab]);
// ...
これは想定された動作であり、コードを壊すことはありません。
この開発時のみの動作は、コンポーネントをピュアに保つのに役立ちます。Reactは呼び出しの1つの結果を使用し、もう1つの結果を無視します。コンポーネントと計算関数がピュアである限り、これはロジックに影響を与えません。ただし、誤って不純なものである場合、これにより間違いに気づき、修正することができます。
たとえば、この不純な計算関数は、プロップとして受け取った配列を書き換えます。
const visibleTodos = useMemo(() => {
// 🚩 Mistake: mutating a prop
todos.push({ id: 'last', text: 'Go for a walk!' });
const filtered = filterTodos(todos, tab);
return filtered;
}, [todos, tab]);
Reactは関数を2回呼び出すため、todoが2回追加されたことに気づきます。計算では既存のオブジェクトを変更するべきではありませんが、計算中に作成した *新しい* オブジェクトは変更しても問題ありません。たとえば、filterTodos
関数が常に *異なる* 配列を返す場合、その配列を書き換えることができます。
const visibleTodos = useMemo(() => {
const filtered = filterTodos(todos, tab);
// ✅ Correct: mutating an object you created during the calculation
filtered.push({ id: 'last', text: 'Go for a walk!' });
return filtered;
}, [todos, tab]);
純粋性について詳しくはコンポーネントをピュアに保つを読んでください。
また、オブジェクトの更新と配列の更新に関するガイドも参照してください。(変更せずに)
私のuseMemo
呼び出しはオブジェクトを返すはずですが、undefinedを返します
このコードは動作しません。
// 🔴 You can't return an object from an arrow function with () => {
const searchOptions = useMemo(() => {
matchMode: 'whole-word',
text: text
}, [text]);
JavaScriptでは、() => {
はアロー関数の本体を開始するため、{
中括弧はオブジェクトの一部ではありません。これがオブジェクトを返さない理由であり、ミスにつながります。({
と})
を追加することで修正できます。
// This works, but is easy for someone to break again
const searchOptions = useMemo(() => ({
matchMode: 'whole-word',
text: text
}), [text]);
ただし、これは依然として分かりにくく、かっこを外すことで簡単に壊れてしまう可能性があります。
この間違いを避けるために、return
文を明示的に記述してください。
// ✅ This works and is explicit
const searchOptions = useMemo(() => {
return {
matchMode: 'whole-word',
text: text
};
}, [text]);
コンポーネントがレンダリングされるたびに、useMemo
内の計算が再実行されます
依存関係配列を2番目の引数として指定したことを確認してください!
依存関係配列を忘れると、useMemo
は計算を毎回再実行します。
function TodoList({ todos, tab }) {
// 🔴 Recalculates every time: no dependency array
const visibleTodos = useMemo(() => filterTodos(todos, tab));
// ...
これは、依存関係配列を2番目の引数として渡す修正版です。
function TodoList({ todos, tab }) {
// ✅ Does not recalculate unnecessarily
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
これが役に立たない場合は、少なくとも1つの依存関係が前のレンダリングとは異なることが問題です。依存関係を手動でコンソールに出力することで、この問題をデバッグできます。
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
console.log([todos, tab]);
その後、コンソールから異なる再レンダリングの配列を右クリックし、両方について「グローバル変数として保存」を選択します。最初のものがtemp1
として保存され、2番目がtemp2
として保存されたと仮定すると、ブラウザコンソールを使用して、両方の配列の各依存関係が同じかどうかを確認できます。
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...
メモ化を中断する依存関係が見つかった場合は、それを削除する方法を見つけるか、それもメモ化します。
ループ内の各リスト項目に対してuseMemo
を呼び出す必要がありますが、許可されていません
Chart
コンポーネントがmemo
でラップされているとします。ReportList
コンポーネントが再レンダリングされる際に、リスト内のすべてのChart
の再レンダリングをスキップしたいと考えています。しかし、ループ内でuseMemo
を呼び出すことはできません。
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 You can't call useMemo in a loop like this:
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure key={item.id}>
<Chart data={data} />
</figure>
);
})}
</article>
);
}
代わりに、各アイテムのコンポーネントを抽出し、個々のアイテムのデータをメモ化します。
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ Call useMemo at the top level:
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure>
<Chart data={data} />
</figure>
);
}
あるいは、useMemo
を削除し、代わりにReport
自体をmemo
でラップすることもできます。item
プロップが変化しない場合、Report
は再レンダリングをスキップするため、Chart
も再レンダリングをスキップします。
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
const data = calculateReport(item);
return (
<figure>
<Chart data={data} />
</figure>
);
});