useCallback
useCallback
は、再レンダー間で関数定義をキャッシュできるようにする React フックです。
const cachedFn = useCallback(fn, dependencies)
リファレンス
useCallback(fn, dependencies)
コンポーネントのトップレベルで useCallback
を呼び出し、再レンダー間で関数定義をキャッシュします。
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
引数
-
fn
: キャッシュしたい関数の値。任意の引数を取り、任意の値を返すことができます。React は初回のレンダー時に、関数を返します(呼び出しません!)。次のレンダー時に、前回のレンダー時からdependencies
が変更されていない場合、React は再び同じ関数を提供します。それ以外の場合は、現在のレンダー時に渡された関数を提供し、後で再利用できる場合に備えて保存します。React は関数を呼び出しません。いつ、どのように呼び出すかをあなたが決定できるように、その関数が返されます。 -
dependencies
:fn
コード内で参照されるすべてのリアクティブ値のリスト。リアクティブ値には、props、state、およびコンポーネント本体内に直接宣言されたすべての変数と関数が含まれます。リンターが React 用に設定されている場合、すべてのリアクティブ値が正しく依存関係として指定されていることを確認します。依存関係のリストには、一定数の項目が含まれ、[dep1, dep2, dep3]
のようにインラインで記述される必要があります。React は、Object.is
比較アルゴリズムを使用して、各依存関係を前回の値と比較します。
返り値
初回のレンダー時、useCallback
は渡された fn
関数を返します。
その後のレンダー時には、前回のレンダーからすでに保存されている fn
関数を返すか(依存関係が変更されていない場合)、このレンダー時に渡された fn
関数を返します。
注意事項
useCallback
はフックですので、コンポーネントのトップレベルまたは独自のフックでのみ呼び出すことができます。ループや条件の中で呼び出すことはできません。それが必要な場合は、新しいコンポーネントを抽出し、その中にその状態を移動させてください。- React は、特定の理由がない限り、キャッシュされた関数を破棄しません。たとえば、開発中には、コンポーネントのファイルを編集すると React はキャッシュを破棄します。開発環境と本番環境の両方で、初回マウント時にコンポーネントが一時停止すると、React はキャッシュを破棄します。将来的に、React はキャッシュを破棄することを活用したさらなる機能を追加するかもしれません。例えば、将来的に React が仮想化リストに対する組み込みサポートを追加する場合、仮想化されたテーブルのビューポートからスクロールアウトした項目のキャッシュを破棄することが理にかなっています。これは、
useCallback
をパフォーマンスの最適化として利用する場合に期待に沿った動作となります。そうでない場合は、state 変数 や ref の方が適切かもしれません。
使い方
コンポーネントの再レンダーをスキップする
レンダーのパフォーマンスを最適化する際には、子コンポーネントに渡す関数をキャッシュする必要があることがあります。まずは、これを実現するための構文を見て、その後、どのような場合に便利かを見ていきましょう。
コンポーネントの再レンダー間で関数をキャッシュするには、その定義を useCallback
フックでラップします。
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
useCallback
には 2 つの要素を渡す必要があります。
- 再レンダー間でキャッシュしたい関数定義。
- 関数内で使用される、コンポーネント内のすべての値を含む依存関係のリスト。
初回のレンダー時に、useCallback
から取得する返される関数は、あなたが渡した関数になります。
次のレンダーでは、React は前回のレンダー時に渡した依存関係と比較します。依存関係が変更されていない場合(Object.is
で比較)、useCallback
は前回と同じ関数を返します。それ以外の場合、useCallback
はこのレンダーで渡された関数を返します。
言い換えると、useCallback
は依存関係が変更されるまでの再レンダー間で関数をキャッシュします。
これが有用な場合を例を通じて見ていきましょう。
例えば、ProductPage
から ShippingForm
コンポーネントに handleSubmit
関数を渡しているとします。
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
theme
プロパティを切り替えるとアプリが一瞬フリーズすることに気付きましたが、JSX から <ShippingForm />
を取り除くと、高速に感じられます。これは ShippingForm
コンポーネントを最適化する価値があることを示しています。
デフォルトでは、コンポーネントが再レンダーされると、React はその子要素全てを再帰的に再レンダーします。これが、ProductPage
が異なる theme
で再レンダーされると、ShippingForm
コンポーネントも再レンダーされる理由です。再レンダーに多くの計算を必要としないコンポーネントにとっては問題ありません。しかし、再レンダーが遅いことを確認した場合、 memo
でラップすることで、props が前回のレンダー時と同じである場合に ShippingForm
に再レンダーをスキップするように指示することができます。
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
この変更により、すべての props が前回のレンダー時と同じ場合、ShippingForm
は再レンダーをスキップします。これが関数のキャッシュが重要になる瞬間です! handleSubmit
を useCallback
なしで定義したとしましょう。
function ProductPage({ productId, referrer, theme }) {
// Every time the theme changes, this will be a different function...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ... so ShippingForm's props will never be the same, and it will re-render every time */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
JavaScriptでは、function () {}
または () => {}
は常に異なる関数を作成します。これは {}
のオブジェクトリテラルが常に新しいオブジェクトを作成するのと似ています。通常、これは問題になりませんが、それは ShippingForm
の props が決して同じにならないということを意味し、あなたの memo
による最適化は機能しないでしょう。これが useCallback
が便利な場面です。
function ProductPage({ productId, referrer, theme }) {
// Tell React to cache your function between re-renders...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...so as long as these dependencies don't change...
return (
<div className={theme}>
{/* ...ShippingForm will receive the same props and can skip re-rendering */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
handleSubmit
を useCallback
でラップすることで、再レンダー間でそれが同じ関数であることを保証します(依存関係が変更されるまで)。それをする特定の理由がない限り、関数を useCallback
でラップする必要はありません。この例では、その理由はそれを memo
でラップされたコンポーネントに渡すことで、再レンダーをスキップできるということです。このページの後半で説明されているように、useCallback
が必要な他の理由もあります。
さらに深く知る
useCallback
と並んで useMemo
をよく見かけることでしょう。子コンポーネントを最適化しようとするとき、どちらも有用です。これらはあなたが下位に渡している何かを memoize(言い換えると、キャッシュ)することを可能にします。
import { useMemo, useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // Calls your function and caches its result
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
その違いはキャッシュできる内容です。
-
useMemo
はあなたの関数の呼び出し結果をキャッシュします。この例では、product
が変更されない限り、computeRequirements(product)
の呼び出し結果をキャッシュします。これにより、ShippingForm
を不必要に再レンダーすることなく、requirements
オブジェクトを下位に渡すことができます。必要に応じて、React はレンダー中にあなたが渡した関数を呼び出して結果を計算します。 -
useCallback
は関数自体をキャッシュします。useMemo
とは異なり、あなたが提供する関数を呼び出しません。代わりに、あなたが提供した関数をキャッシュして、productId
またはreferrer
が変更されない限り、handleSubmit
自体が変更されないようにします。これにより、ShippingForm
を不必要に再レンダーすることなく、handleSubmit
関数を下位に渡すことができます。ユーザーがフォームを送信するまであなたのコードは実行されません。
すでに useMemo
に詳しい場合、useCallback
を次のように考えると役立つかもしれません。
// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
さらに深く知る
あなたのアプリがこのサイトのように、ほとんどのインタラクションが大まかなもの(ページ全体やセクション全体の置き換えなど)である場合、メモ化は通常不要です。一方、あなたのアプリが描画エディターのようなもので、ほとんどのインタラクションが細かい(形状の移動など)場合、メモ化は非常に役立つかもしれません。
useCallback
で関数をキャッシュすることは、いくつかのケースで有用です。
- それを
memo
でラップされたコンポーネントにプロパティとして渡すケース。値が変わらなければ、再レンダーをスキップしたいと考えます。メモ化により、依存関係が変更された場合にのみ、コンポーネントが再レンダーされます。 - あなたが渡している関数が、後で何らかのフックの依存関係として使用されるケース。たとえば、他の
useCallback
でラップされた関数がそれに依存している、またはuseEffect
からこの関数に依存しているケースです。
その他のケースで関数を useCallback
でラップする利点はありません。それを行っても重大な害はないため、一部のチームは個々のケースについて考えず、可能な限り多くをメモ化することを選択します。欠点は、コードが読みにくくなることです。また、すべてのメモ化が効果的なわけではありません。コンポーネント全体のメモ化を破壊するには「常に新しい」単一の値だけで十分です。
useCallback
は関数の作成を防ぐわけではないことに注意してください。あなたは常に関数を作成しています(それは問題ありません!)。しかし、何も変わらない場合、Reactはそれを無視し、キャッシュされた関数を返します。
実際には、以下のいくつかの原則に従うことで、多くのメモ化を不要にすることができます。
- コンポーネントが他のコンポーネントを視覚的にラップするときは、それが子として JSX を受け入れるようにします。 すると、ラッパーコンポーネントが自身の状態を更新すると、React はその子が再レンダーする必要がないことを認識します。
- ローカル状態を優先し、必要以上に state を引き上げないでください。フォームや、アイテムがホバーされているかどうかのような一時的な状態をツリーのトップやグローバル状態ライブラリに保持しないでください。
- レンダーロジックを純粋に保ちます。コンポーネントの再レンダーが問題を引き起こしたり、何らかの目に見える視覚的な結果を生じたりする場合、それはあなたのコンポーネントのバグです!メモ化を追加するのではなく、バグを修正します。
- 状態を更新する不要な副作用を避けてください。 React アプリケーションのパフォーマンス問題の大部分は、コンポーネントのレンダーを何度も繰り返させる副作用を起源とする、更新の連鎖によって引き起こされます。
- 不要な依存関係を副作用から削除してみてください。 例えば、メモ化の代わりに、あるオブジェクトや関数を副作用内部やコンポーネント外部に移動させる方が簡単です。
特定のインタラクションの遅延をまだ感じる場合は、React Developer Tools のプロファイラを使用してどのコンポーネントが最もメモ化から利益を得るかを確認し、必要な場所にメモ化を追加してください。これらの原則はコンポーネントのデバッグと理解を容易にするため、どの場合でもこれらに従うことは良いです。長期的には、自動的にメモ化を行うことを研究していて、これによってこの問題を一度にすべて解決することを目指しています。
例 1/2: useCallback
と memo
を使用して再レンダーをスキップする
この例では、ShippingForm
コンポーネントが人為的に遅延させられているため、あなたがレンダーしている React コンポーネントが本当に遅いときに何が起こるかを見ることができます。カウンターを増加させたり、テーマを切り替えてみてください。
カウンターの増加は遅く感じられるでしょう。なぜなら、それは遅延させられた ShippingForm
の再レンダーを強制するからです。これは、カウンターが変更され、ユーザーの新しい選択を画面上に反映する必要があるため、予想される動作です。
次に、テーマの切り替えを試してみてください。人為的な遅延にも関わらず、useCallback
と memo
を組み合わせることで、これは速いです!ShippingForm
は、handleSubmit
関数が変更されていないため、再レンダーをスキップしました。handleSubmit
関数は変更されていません。なぜなら、productId
と referrer
(あなたの useCallback
の依存関係)の両方が最後のレンダー以降に変更されていないからです。
import { useCallback } from 'react'; import ShippingForm from './ShippingForm.js'; export default function ProductPage({ productId, referrer, theme }) { const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }, [productId, referrer]); return ( <div className={theme}> <ShippingForm onSubmit={handleSubmit} /> </div> ); } function post(url, data) { // Imagine this sends a request... console.log('POST /' + url); console.log(data); }
メモ化されたコールバックからの状態更新
場合によっては、メモ化されたコールバックから前の状態に基づいて状態を更新する必要があるかもしれません。
この handleAddTodo
関数は、次の todos を計算するために todos
を依存性として指定します。
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
通常、メモ化された関数は可能な限り依存性を少なくしたいと思うでしょう。次の状態を計算するためだけにいくつかの状態を読み込む場合、代わりに更新関数を渡すことでその依存関係を削除できます。
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ No need for the todos dependency
// ...
ここでは、todos
を依存関係として内部で読み込む代わりに、どのように状態を更新するかについての指示(todos => [...todos, newTodo]
)を React に渡します。更新関数についての詳細はこちら。
副作用が頻繁に発火するのを防ぐ
時々、副作用 の内部から関数を呼び出したいことがあるかもしれません。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
// ...
これには問題があります。全てのリアクティブな値は副作用の依存関係として宣言されなければなりません。 しかし、createOptions
を依存関係として宣言すると、あなたの副作用がチャットルームに常に再接続することになります。
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problem: This dependency changes on every render
// ...
これを解決するために、Efffect から呼び出す必要がある関数を useCallback
でラップすることができます。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Only changes when roomId changes
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Only changes when createOptions changes
// ...
これにより、roomId
が同じ場合に再レンダー間で createOptions
関数が同じであることが保証されます。しかし、関数の依存性を必要としないようにすることがさらに良いです。関数をエフェクトの内部に移動します。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ No need for useCallback or function dependencies!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Only changes when roomId changes
// ...
これであなたのコードはよりシンプルになり、useCallback
が不要になりました。副作用の依存関係の削除についてさらに学びましょう。
カスタムフックの最適化
あなたがカスタムフックを書いている場合、それが返す任意の関数を useCallback
でラップすることが推奨されます。
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
}
これにより、フックの使用者が必要に応じて自身のコードを最適化することができます。
トラブルシューティング
コンポーネントがレンダーするたびに useCallback
が異なる関数を返す
第 2 引数として依存関係の配列を指定したかを確認してください!
依存関係の配列を忘れると、 useCallback
は毎回新しい関数を返します。
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Returns a new function every time: no dependency array
// ...
以下は、第 2 引数として依存関係の配列を渡す修正版です。
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ Does not return a new function unnecessarily
// ...
これが役に立たない場合、問題は、少なくとも 1 つの依存関係が前回のレンダーと異なることです。依存関係を手動でコンソールにログ出力することで、この問題をデバッグできます。
const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);
console.log([productId, referrer]);
その後、コンソール内の異なる再レンダーからの配列を右クリックし、それぞれに対して「グローバル変数として保存」を選択できます。最初のものが temp1
として、2 つ目が temp2
として保存されたと仮定すると、ブラウザのコンソールを使用して、両方の配列内の各依存関係が同一であるかどうかを確認できます。
Object.is(temp1[0], temp2[0]); // 配列間で最初の依存関係は同じですか?
Object.is(temp1[1], temp2[1]); // 2 番目の依存関係は同じですか?
Object.is(temp1[2], temp2[2]); // 依存関係があるすべてのものについて続けます...
メモ化を壊している依存関係を見つけたら、それを取り除く方法を見つけるか、またはそれもメモ化します。
ループ内の各リスト要素で useCallback
を呼び出す必要があるが、それは許されていない
Chart
コンポーネントが memo
でラップされていると仮定します。ReportList
コンポーネントが再レンダーするときに、リスト内のすべての Chart
の再レンダーをスキップしたいとします。しかし、ループの中で useCallback
を呼び出すことはできません。
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 You can't call useCallback in a loop like this:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}
その代わりに、個々のアイテムのコンポーネントを抽出し、その中に useCallback
を配置します。
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ Call useCallback at the top level:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
もしくは、最後のスニペットから useCallback
を削除し、代わりに Report
自体を memo
でラップすることもできます。item
プロパティが変更されない場合、Report
は再レンダーをスキップするため、Chart
も再レンダーをスキップします。
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});