useMemo
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
が変化していなければ、同じ値を再度返します。dependencies
が変化していれば、calculateValue
を呼び出してその結果を返し、同時に、後から再利用するためにその結果を保存します。 -
dependencies
:calculateValue
のコード内で参照されているすべてのリアクティブ値の配列。リアクティブ値には、props、state、およびコンポーネント本体で直接宣言されているすべての変数と関数が含まれます。リンタが React 向けに設定されている場合は、すべてのリアクティブ値が正しく依存値として指定されているかを確認します。依存配列は、[dep1, dep2, dep3]
のようにインラインで記述され、配列の長さは一定である必要があります。各依存値は、Object.is
を用いて、前回の値と比較されます。
返り値
初回のレンダーでは、引数なしで calculateValue
を呼び出した結果が、useMemo
の返り値となります。
次回以降のレンダーでは、依存配列が変化していない場合は、以前のレンダーで保存された値を返します。変化している場合は、calculateValue
を再度呼び出し、その結果をそのまま返します。
注意点
useMemo
はフックなので、カスタムフックかコンポーネントのトップレベルでしか呼び出すことができません。ループや条件分岐の中で呼び出すことはできません。もしループや条件分岐の中で呼び出したい場合は、新しいコンポーネントに切り出して、その中に state を移動させてください。- Strict Mode では、純粋でない関数を見つけやすくするために、計算関数 (
calculateValue
) が 2 度呼び出されます。これは、開発時のみの挙動で、本番では影響は与えません。もし、計算関数が純粋であれば(純粋であるべきです)、2 回呼び出されてもコードに影響はありません。2 回の呼び出しのうち、一方の呼び出し結果は無視されます。 - 特別な理由がない限り、キャッシュされた値が破棄されることはありません。キャッシュが破棄されるケースの例としては、開発時にコンポーネントのファイルを編集した場合があります。また、開発時および本番時に、初回マウント中にコンポーネントがサスペンドすると、キャッシュは破棄されます。将来的には、キャッシュが破棄されることを前提とした機能が React に追加される可能性があります。例えば、将来的に仮想リストが組み込みでサポートされた場合、仮想テーブルのビューポートからスクロールアウトした項目は、キャッシュを破棄するようになるかもしれません。このような挙動は、パフォーマンス最適化のみを目的として
useMemo
を使っている場合には問題ありません。しかし、他の目的で利用している場合は、state 変数 や ref を利用した方が良いかもしれません。
使用法
高コストな再計算を避ける
複数レンダーを跨いで計算をキャッシュするには、コンポーネントのトップレベルで useMemo
を呼び出し、計算をラップします。
import { useMemo } from 'react';
function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}
useMemo
には、2 つの引数を渡す必要があります。
() =>
のように、引数を取らず、求めたい計算結果を返す計算関数。- コンポーネント内にある値のうち、計算関数内で使用されているすべての値を含む、依存配列。
初回レンダーでは、useMemo
から返される値は、計算関数を呼び出した結果になります。
次回以降のレンダーでは、今回のレンダー時に渡した依存配列と、前回のレンダー時に渡した依存配列が比較されます。(Object.is
で比較します。)依存値のいずれも変化していない場合、useMemo
は以前に計算した値を返します。変化している場合は、再度計算が実行され、新しい値が返されます。
つまり useMemo
は、依存配列が変化しない限り、複数のレンダーを跨いで計算結果をキャッシュします。
これが役に立つ場面を見てみましょう。
React では通常、再レンダーが発生するたびに、コンポーネント関数全体が再実行されます。例えば、以下の TodoList
で、state が更新されたり、親から新しい 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 スロットリングオプションが提供されています。
また、開発環境でのパフォーマンス測定では完全に正確な結果は得られないことに注意してください。(例えば、Strict Mode がオンの場合、各コンポーネントが 1 度ではなく 2 度レンダーされることがあります。)最も正確にパフォーマンスを計測するためには、アプリを本番環境用にビルドし、ユーザが持っているようなデバイスでテストしてください。
さらに深く知る
あなたのアプリがこのサイトのように、ほとんどのインタラクションが大まかなもの(ページ全体やセクション全体の置き換えなど)である場合、メモ化は通常不要です。一方、あなたのアプリが描画エディタのようなもので、ほとんどのインタラクションが細かなもの(図形を移動させるなど)である場合、メモ化は非常に役に立つでしょう。
useMemo
を利用した最適化が力を発揮するのは、以下のような、ほんの一部のケースに限られます。
useMemo
で行う計算が著しく遅く、かつ、その依存値がほとんど変化しない場合。- 計算した値を、
memo
でラップされたコンポーネントの props に渡す場合。この場合は、値が変化していない場合には再レンダーをスキップしたいでしょう。メモ化することで、依存値が異なる場合にのみコンポーネントを再レンダーさせることができます。 - その値が、後で何らかのフックの依存値として使用されるケース。例えば、別の
useMemo
の計算結果がその値に依存している場合や、useEffect
がその値に依存している場合などです。
これらのケース以外では、計算を useMemo
でラップすることにメリットはありません。それを行っても重大な害はないため、個別のケースを考えずに、可能な限りすべてをメモ化するようにしているチームもあります。このアプローチのデメリットは、コードが読みにくくなるという点です。また、すべてのメモ化が有効であるわけではありません。例えば、毎回変化する値が 1 つ存在するだけで、コンポーネント全体のメモ化が無意味になってしまうこともあります。
実際には、以下のいくつかの原則に従うことで、多くのメモ化を不要にすることができます。
- コンポーネントが他のコンポーネントを視覚的にラップするときは、それが子として JSX を受け入れるようにします。これにより、ラッパコンポーネントが自身の state を更新しても、React はその子を再レンダーする必要がないことを認識します。
- ローカル state を優先し、必要以上に state のリフトアップを行わないようにします。フォームや、アイテムがホバーされているかどうか、といった頻繁に変化する state は、ツリーのトップやグローバルの状態ライブラリに保持しないでください。
- レンダーロジックを純粋に保ちます。コンポーネントの再レンダーが問題を引き起こしたり、何らかの目に見える視覚的な結果を生じたりする場合、それはあなたのコンポーネントのバグです! メモ化を追加するのではなく、バグを修正します。
- state を更新する不要なエフェクトを避けてください。React アプリケーションのパフォーマンス問題の大部分は、エフェクト内での連鎖的な state 更新によってコンポーネントのレンダーが何度も引き起こされるために生じます。
- エフェクトから不要な依存値をできるだけ削除します。例えば、メモ化する代わりに、オブジェクトや関数をエフェクトの中や外に移動させるだけで、簡単に解決できる場合があります。
それでも特定のインタラクションが遅いと感じる場合は、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
コンポーネントが、子コンポーネントの List
の props として、visibleTodos
を渡すことを考えます。
export default function TodoList({ todos, tab, theme }) {
// ...
return (
<div className={theme}>
<List items={visibleTodos} />
</div>
);
}
props である theme
を変化させると一瞬アプリがフリーズしますが、<List />
を JSX から削除すると、高速に動作するようになったはずです。すなわち、この List
コンポーネントには最適化する価値があるということです。
通常、あるコンポーネントが再レンダーされたときは、その子コンポーネントも再帰的にすべて再レンダーされます。これが、TodoList
が異なる theme
の値で再レンダーされたとき、List
コンポーネントも一緒に再レンダーされる理由です。この動作は、再レンダーにそれほど多くの計算コストを必要としないコンポーネントには適しています。しかし、もし再レンダーが遅いと分かった場合は、List
コンポーネントを memo
で囲うことで、与えられた props が前回のレンダーと同じである場合に List
の再レンダーをスキップすることができます。
import { memo } from 'react';
const List = memo(function List({ items }) {
// ...
});
この変更によって、props の全項目が前回のレンダーと等しい場合には、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
の props が毎回別の値になってしまいます。そのため、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
を追加する動機はいくつかあり、このページで詳しく解説していきます。
さらに深く知る
List
を memo
でラップする代わりに、<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 ノードがイミュータブル (immutable) であるためです。JSX ノードオブジェクトは時間が経過しても変化することはないため、再レンダーをスキップしてしまって問題ありません。しかし、これが機能するには、ノードが真に全く同一のオブジェクトである必要があり、コード上で同じように見えるだけでは不十分です。この例では、useMemo
のおかげで、ノードが全く同じオブジェクトとなっているのです。
useMemo
を使って、JSX ノードを手動でラップするのは不便です。例えば、条件付きでラップすることはできません。そのため、通常は useMemo
で JSX ノードをラップする代わりに、memo
でコンポーネントをでラップします。
例 1/2: useMemo
と memo
を利用して再レンダーをスキップする
この例では、List
コンポーネントには人為的な遅延が入っています。そのため、レンダー中に呼び出している React コンポーネントが著しく遅い場合の挙動を確認できます。タブを変更したり、テーマを切り替えたりしてみてください。
タブの切り替えが遅く感じるのは、遅延が入っている List
を再レンダーさせてしまっているからです。これは考えてみれば当然で、tab
が変化したので、ユーザの新しい選択を画面に反映する必要があります。
次に、テーマを切り替えてみましょう。useMemo
と memo
があるおかげで、人為的な遅延があるにも関わらず、高速に動作しています! visibleItems
配列が前回のレンダー時から変化していないため、List
は再レンダーをスキップしています。visibleItems
配列が変化していないのは、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> ); }
エフェクトが過度に実行されるのを抑制する
エフェクト内で、以下のようにして何らかの値を使用したくなる場合があります。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = {
serverUrl: 'https://localhost:1234',
roomId: roomId
}
useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...
しかしこれにより問題が生じます。すべてのリアクティブ値はエフェクト内で依存値として宣言する必要があります。しかしこの options
を依存値として宣言してしまうと、エフェクトがチャットルームへの再接続を繰り返すようになってしまいます。
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // 🔴 Problem: This dependency changes on every render
// ...
これを修正するために、エフェクト内で使用されるオブジェクトを 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 connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Only changes when options changes
// ...
これで、useMemo
がキャッシュ済みのオブジェクトを返している限り、options
オブジェクトが再レンダー間で等しくなることが保証されます。
しかし useMemo
はパフォーマンス最適化のためのものであり、意味的な保証があるものではありません。特定の理由がある場合は、React はキャッシュされた値を破棄することがあります。これによりエフェクトも再実行されるため、エフェクト内にオブジェクトを移動することでこのような依存値自体を不必要にする方がより良いでしょう。
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
も不要となりました。エフェクトから依存値を取り除く方法について参照してください。
他のフックに渡す依存値をメモ化する
ある計算が、コンポーネントの本体で直接作成されたオブジェクトに依存しているとしましょう。
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
の依存値であり、毎回異なる値となるため、依存値が変化したと判断され、searchItems
が毎回再計算されます。
これを修正するには、searchOptions
オブジェクトを依存配列に渡す前に、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
に依存するようになりました。(text
は文字列なので「意図せず」変化してしまうことはありません。)
関数をメモ化する
Form
コンポーネントが memo
でラップされているとします。関数を props として渡してみましょう。
export default function ProductPage({ productId, referrer }) {
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}
return <Form onSubmit={handleSubmit} />;
}
{}
が異なるオブジェクトを生成するのと同様に、function() {}
のような関数宣言や、() => {}
のような関数式もまた、レンダーごとに異なる関数を生成します。新しい関数が生成されること自体は問題ではなく、避けるべきことでもありません。しかし、Form
コンポーネントがメモ化されている状況では、Form
の props に渡す値が変わっていない場合は Form
の再レンダーをスキップしたいと考えるでしょう。毎回異なる値が props にあると、メモ化は無意味になってしまいます。
useMemo
で関数をメモ化する場合は、計算関数がさらに別の関数を返す必要があります。
export default function Page({ productId, referrer }) {
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
};
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
なんだか不恰好ですね! 関数のメモ化はよくあることなので、それ専用の組み込みフックが提供されています。余計な関数の入れ子を避けるには、useMemo
の代わりに useCallback
で関数をラップしましょう。
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 回実行される
Strict Mode では、本来 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]);
// ...
これは想定通りの挙動であり、これでコードが壊れることがあってはいけません。
これは開発時のみの挙動で、開発者がコンポーネントを純粋に保つために役立ちます。呼び出し結果のうちの 1 つが採用され、もう 1 つは無視されます。あなたが実装したコンポーネントと計算関数が純粋であれば、この挙動がロジックに影響を与えることはありません。しかし、もし意図せず純粋ではない関数になっていた場合は、この挙動によって間違いに気づき、修正することができます。
たとえば、以下の計算関数は、props として受け取った配列の書き換え(ミューテーション)をしてしまっており、純粋ではありません。
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]);
しかし、この関数は 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]);
コンソール上で、別々の再レンダーによって表示された 2 つの配列を選びます。それぞれについて、配列を右クリックし、“Store as a global variable(グローバル変数として保存)” を選択することで、配列を保存することができます。1 回目に保存した配列が 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>
);
});