useCallback
は、再レンダリング間で関数定義をキャッシュするReactのフックです。
本稿はReact公式サイト「useCallback
」にもとづき、useCallbackはどう使うのか、およびどのような場合に使うとよいのかを解説します。説明内容と順序は、公式ドキュメントにしたがいました。ただし、解説はわかりやすく改め、またコード例とサンプル(StackBlitz)はTypeScriptを加えたうえで修正した部分が少なくありません。
構文
const cachedFn = useCallback(fn, dependencies)
useCallback
はコンポーネントのトップレベルで呼び出して、再レンダリング間で関数の定義をキャッシュします。
import { useCallback } from 'react';
import type { FC } from 'react';
type Props = {
};
export const ProductPage: FC<Props> = ({ productId, referrer, theme }) => {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
};
引数
-
fn
: キャッシュする関数型の値。渡す引数も戻り値も任意です。Reactは、まず最初のレンダー時には関数をそのまま返します(呼び出しはしません)。次回以降のレンダリングについてはつぎのとおりです。- 直前のレンダー時と依存値(第2引数の配列要素値)が変わっていないとき: Reactは前と同じ関数を返します。
- 直前のレンダー時から依存値が変わった場合: 返されるのは今回のレンダー時に渡された関数です。そして、再利用に備えて保存されます。Reactは関数を返すだけで、呼び出しはしません。関数をいつ呼び出すかは、開発者が決めることです。
-
dependencies
: 第1引数fn
のコード内で参照されるすべてのリアクティブ値の(依存)配列。リアクティブな値に含まれるのは、プロパティと状態、およびコンポーネント本体に直接宣言された変数と関数です。React用に設定されたリンターであれば、リアクティブな値がすべて依存関係に正しく指定されているかを確かめます。依存配列には、特定の依存値が要素として含まれなければなりません。インラインで、[dep1
,dep2
,dep3
]という記述です。Reactは、各依存値を直前の値とObject.is
メソッドで比較します。
戻り値
最初のレンダー時は、useCallback
に渡された引数の関数fn
です。
以降のレンダーでは、依存値が変わったかどうかにより、返す値は変わります。
- 依存値に変更がない場合: 返されるのは前のレンダリング時に保存した関数です。
- 依存値が変わったとき: このレンダー時に渡された関数
fn
が返されます。
注意
-
useCallback
はフックなので、呼び出せるのはコンポーネントまたはカスタムフックのトップレベルからのみです。ループ文や条件文の中からは呼び出しできません。それが必要なときは、コンポーネントを新たに切り出し、状態はその中に移してください。 - Reactは、特別な理由がないかぎり、キャッシュされた値を破棄しません。開発環境ならReactは、たとえばコンポーネントのファイルが編集されたら、キャッシュを破棄します。開発と本番の両環境で、Reactがキャッシュを破棄するのは、コンポーネントの初期マウント完了に至らなかった場合です。
Reactは、将来的にキャッシュの破棄が利用できる機能をさらに追加することもありえます。たとえば、仮想化リストの組み込みサポートを加えるようになった場合です。仮想化されたテーブルビューポートからスクロールアウトするアイテムのキャッシュを破棄することは意味があるでしょう。
パフォーマンスの最適化が目的でuseCallback
を用いることは問題ありません。そうでない場合は、状態変数やref
を活用する方がより適切なこともありえます。
使い方
コンポーネントの再レンダーを省く
子コンポーネントに渡す関数をuseCallback
でキャッシュする
レンダーのパフォーマンス最適化のために、子コンポーネントに渡す関数をキャッシュすることが役立つかもしれません。コンポーネントの再レンダー間で関数をキャッシュするには、その定義をuseCallback
フックで包んでください。
export const ProductPage: FC<Props> = ({ productId, referrer, theme }) => {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
};
useCallback
に渡す引数はふたつです。
- 関数定義: 再レンダー間でキャッシュする関数。
- 依存関係のリスト: 関数が用いるコンポーネント内のすべての依存値を配列で渡します。
初期レンダー時: useCallback
から返される関数は、フックを呼び出すときに渡した第1引数の定義そのものです。
次回以降のレンダー時: Reactは各レンダーごとに、依存関係を直近に渡された依存値と比較します。依存関係に(Object.is()による比較で)変わりなければ、useCallback
が返すのは前回と同じ関数です。変更があったら、フックは今回のレンダーで渡された新たな関数を返します。
つまり、useCallback
は、依存関係が変わらないかぎり、再レンダリングの間関数定義をキャッシュするのです。
useCallback
でキャッシュした関数をmemo
に包んだコンポーネントへ渡す
useCallback
がどう役立つのか、コード例で見ていきましょう。つぎのモジュールsrc/ProductPage.tsx
は、子コンポーネントShippingForm
にonSubmit
プロパティの値として関数handleSubmit
を渡しています。
export const ProductPage: FC<Props> = ({ productId, referrer, theme }) => {
const handleSubmit: (orderDetails: OrderDetails) => void = (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
};
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
};
また、ProductPage
コンポーネントのプロパティtheme
は、親(App.tsx
)から受け取った値です。
export default function App() {
const [isDark, setIsDark] = useState(false);
return (
<>
<label>
<input
type="checkbox"
checked={isDark}
onChange={({ target: { checked } }) => setIsDark(checked)}
/>
Dark mode
</label>
<hr />
<ProductPage
theme={isDark ? 'dark' : 'light'}
/>
</>
);
}
つぎのサンプル001で親コンポーネントのチェックボックス(<input type="checkbox"
)を切り替えたとき、画面カラーの反応が遅く感じるかもしれません(あえて、ShippingForm
コンポーネントのレンダーに遅延を加えました)。
サンプル001■React + TypeScript: useCallback 01
デフォルトでは、コンポーネントがレンダーされると、Reactはその子要素すべてを再帰的に再レンダーします。そのため、ProductPage
が異なるtheme
でレンダリングし直されると、ShippingForm
コンポーネントも再レンダーされるのです。
もっとも、レンダリング負荷の少ないコンポーネントであれば、とくに問題ありません。再レンダーはできるだけ省きたいというとき、ひとつ考えられるのはコンポーネントをmemo
でラップすることです。渡されたプロパティ(props
)が前回のレンダー時と同じ場合には、再レンダーを省けます。
import { memo, useState } from 'react';
export const ShippingForm: FC<Props> = memo(({ onSubmit }) => {
});
コンポーネントShippingForm
の受け取るプロパティ(props
)がすべて同じなら、再レンダーされなくなりました。ただし、ここで重要になってくるのが関数のキャッシュです。まだ、ProductPage
コンポーネントが子へ渡す関数(handleSubmit
)にuseCallback
は使っていません。
export const ProductPage: FC<Props> = ({ productId, referrer, theme }) => {
const handleSubmit: (orderDetails: OrderDetails) => void = (orderDetails) => {
};
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
};
このままでは、ProductPage
が再レンダーされるたびに、handleSubmit
は新たな関数定義として扱われてしまいます。オブジェクトリテラルで、中身はまったく同じでも、参照が異なればJavaScriptからは別物とみなされるのと同じです。コンポーネントのプロパティ(props
)として渡される関数も、中身が変わらなくても、同じとは評価されません。
そこで、必要なのがuseCallback
です。フックで包めば、依存配列が変わらないかぎり、つねに同じ関数が返されます。
export const ProductPage: FC<Props> = ({ productId, referrer, theme }) => {
// const handleSubmit: (orderDetails: OrderDetails) => void = (orderDetails) => {
const handleSubmit: (orderDetails: OrderDetails) => void = useCallback(
(orderDetails) => {
},
[productId, referrer]
);
};
この変更を加えたサンプル002は、チェックボックス(theme
)の反応が速くなったでしょう。この例は、useCallback
でラップした関数を、memo
に包んだコンポーネントへ渡す手法のご紹介でした。ただ、とくに理由がなければuseCallback
は使わなくて構いません。
サンプル002■React + TypeScript: useCallback 02
[注記] useCallback
はパフォーマンスを最適化するためにお使いください。フックなしにはコードが動かないという場合は、まず原因を探って修正するべきです。そのうえで、必要があればuseCallback
を用いましょう。
useCallback
とuseMemo
useCallback
と似た機能を果たすフックにuseMemo
があります。どちらも、子コンポーネントを最適化するために有用です。両フックともに、プロパティとして渡す値をメモ化できます。
import { useMemo, useCallback } from 'react';
export const ProductPage: FC<Props> = ({ productId, referrer, theme }) => {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // 関数を呼び出して結果はキャッシュする
return computeRequirements(product);
}, [product]);
const handleSubmit: (orderDetails: OrderDetails) => void = useCallback(
(orderDetails) => {
},
[productId, referrer]
);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
};
ふたつのフックの違いは、キャッシュする中身です。
-
useMemo
: 引数の関数を呼び出した戻り値がキャッシュされます。- 上記コード例では、依存配列の値
product
が変わらないかぎり、computeRequirements(product)
は、再呼び出しされません。前にキャッシュした戻り値が用いられるのです。 -
requirements
プロパティを渡された子コンポーネントShippingForm
は、無駄に再レンダーされません。 - 依存値
product
が変わったとき、Reactはレンダー中に引数の関数を再呼び出して返します。
- 上記コード例では、依存配列の値
-
useCallback
: キャッシュするのは引数の関数そのものです。関数は呼び出しません。- 上記コード例では、依存配列の値
productId
またはreferrer
が変わらないかぎり、関数handleSubmit
は、キャッシュした定義が用いられます。 -
onSubmit
のコールバックとしてhandleSubmit
を渡された子コンポーネントShippingForm
は、無駄に再レンダーされません。 - 関数
handleSubmit
が呼び出されるのは、子コンポーネントShippingForm
の[Submit]ボタンを押したとき(onSubmit
イベント)です。
- 上記コード例では、依存配列の値
すでにuseMemo
フックをご存じであれば、useCallback
はつぎのように捉えればよいでしょう(「関数をメモ化する」参照)。なお、TypeScriptによる型づけは省きました(興味のある方は「useCallback」をお読みください)。
// 簡略化したReact内部の実装
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
useCallback
はどこに使うべきか
メモ化はどこに用いるべきで、どういうときは不要か、大まかに比べるなら大きくふたつです。
- インタラクションがさほど多くなければ、
useCallback
を使わななくて構いません。- ページの切り替えや画面の一部の変更で済む場合。
- 細かなインタラクションが頻繁に生じるときは、メモ化は有効でしょう。
- 描画エディターのアプリケーションで、図形を移動させるなど。
useCallback
で関数をキャッシュすることが役立つ場合
useCallback
で関数をキャッシュすることが役立つのは、具体的にはつぎのふたつの例でしょう。
-
memo
で包まれた子コンポーネントに関数をプロパティ(props
)として渡す場合。-
useCallback
の依存値が変わらないかぎり、子コンポーネントは再レンダーされない。 - 依存値が変わったときだけ、レンダリングされる。
-
- 関数が他のフックの依存値に用いられている場合。
- 他の
useCallback
でラップされた関数が依存している。 -
useEffect
の依存配列に関数が含まれる。
- 他の
これらの場合以外は、関数をuseCallback
でラップしても、効果はないでしょう。かといって、問題となることも考えにくいです。とりあえず、メモ化してしまうという開発チームもあります。デメリットとしては、コードが読みにくくなることです。また、レンダーごとに変わる値がひとつでも関数に含まれていれば、メモ化する意味はありません。
useCallback
は、関数をつくらないわけではありません。関数はつねに定義されます(それは問題とはなりません)。ただし、前回のレンダーと変わりなければ、Reactにより新たな定義は無視され、キャッシュされた関数が返されるのです。
不要なメモ化を避けるための5つの原則
メモ化はつねに有効とはかぎりません。つぎの5つの原則にしたがえば、不要なメモ化が避けられるでしょう。
- コンポーネントが子コンポーネントを視覚的にラップするときは、子のJSXは
children
として受け取るようにします。子を包む親コンポーネントが自身の状態を更新しても、Reactは子の再レンダーは要らないとわかるからです。 - 状態はできるだけローカルに持ち、無闇にコンポーネントツリー上を引き上げないようにします(「React + TypeScript: コンポーネント間で状態を共有する」参照)。フォーム入力や項目へのホバーといった状態は、頻繁に変わるインタラクションです。ツリーのトップやグローバルの状態ライブラリに保持しないでください。
- レンダーロジックを純粋に保ちましょう。コンポーネントの再レンダーで意図しない動きになったり、表示に明らかな問題が起こるとすれば、それはバグです。メモ化で避けようとするのでなく、原因をつきとめて修正してください。
- 不必要に状態を更新するエフェクトは避けましょう。Reactアプリケーションでパフォーマンスの問題を引き起こす原因の多くが、エフェクトによる連鎖的な状態の更新です。コンポーネントはそのたびに再レンダーされてしまいます。
- エフェクトから要らない依存値は除いてください。メモ化しなくても、オブジェクトや関数をエフェクトの中あるいは外に移すだけで、簡単に解決できることもあります。
以上の原則にしたがっても、遅いと感じるインタラクションが残るかもしれません。そのような場合には、React Developer ToolsのProfilerパネルをお使いください。どのコンポーネントをメモ化すればもっとも効果的かが確かめられます。このように開発を進めることで、デバッグしやすく、わかりやすいコードが書けるでしょう。React公式サイトが推奨する理由です。将来に向けては、自動的にメモ化する研究も進められています(「React without memo」参照)。
関数にuseCallback
を用いるかどうかによる違い
前掲サンプル001はuseCallback
なし、サンプル002にはuseCallback
を用いました。どちらも、コンポーネントShippingForm
は、memo
で包んでいます。また、ShippingForm
の描画には、あえて負荷をかけました。ふたつのサンプルの違いを改めて確かめましょう。
useCallback
で子コンポーネントの無駄な再レンダーを省いた場合
サンプル002では、親コンポーネント(App.tsx
)のチェックボックス([Dark mode])で切り替えるプロパティtheme
の画面への反映は速やかです。プロパティを渡されたコンポーネントProductPage
のhandleSubmit
には、useCallback
が用いられており、theme
の値には依存しません。したがって、子コンポーネントShippingForm
に与えるプロパティhandleSubmit
は変わらず、再レンダーされないからです。memo
によるShippingForm
のラップが有効に働きました。
他方で、カウンターの増減は反応が遅いです。カウンターの値(count
)はShippingForm
コンポーネントの状態なので、再レンダーせざるを得ません。このコード例では予期された動きです。
関数にuseCallback
を使わない場合
サンプル001でも、カウンターの増減は反応が遅いです(理由はサンプル002と同じ)。さらに、チェックボックス([Dark mode])で切り替えるプロパティtheme
の画面への反映まで遅延します。useCallback
を使わない関数handleSubmit
は、処理がまったく変わらなくても、つねに新たな関数として定義されるからです。ShippingForm
コンポーネントは、そのたびに再レンダーされます。
ここで、ShippingForm
コンポーネントの意図的な遅延を除いてみましょう。反応の遅れは気にするほどのものですか。インタラクションの速さが問題にならないかぎり、あえてメモ化するには及びません。
export const ShippingForm: FC<Props> = ({ onSubmit }) => {
/* let startTime = performance.now();
while (performance.now() - startTime < 500) {
// 意図的にコードを遅くするため500ミリ秒何もしない
} */
};
なお、アプリケーションの遅れを根本的に解決するには、Reactは本番モードで実行すべきです。React Developer Toolsも無効にし、想定するユーザーの実機で確かめなければなりません。
メモ化されたコールバックから状態を更新する
メモ化されたコールバックから、直前の状態を更新したい場合があり得ます。たとえば、つぎのコード例のhandleAddTodo
関数です。新たなTodoリストをつくるため、状態変数todos
に依存します。
const TodoList = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const handleAddTodo = useCallback((text: string) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
};
メモ化した関数の依存値は、できるかぎり減らしたいところです。直近の状態から新たな状態を単純に定める場合、状態設定関数(setTodos
)には更新用関数が渡せます。関数が引数に受け取るのは、現在の状態値です。これで、useCallback
から状態変数todos
への依存が除けます。
const TodoList = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const handleAddTodo = useCallback((text: string) => {
const newTodo = { id: nextId++, text };
// setTodos([...todos, newTodo]);
setTodos((todos) => [...todos, newTodo]);
// }, [todos]);
}, []); // ✅ todosへの依存は不要
};
エフェクトが無駄に実行されるのを防ぐ
エフェクトの中から関数を呼び出したいこともあるでしょう。けれど、つぎのコードは問題です。すべてのリアクティブな値は、依存配列に含めなければなりません。createOptions
を依存値として宣言すると、エフェクトがつねにチャットルームに再接続してしまうからです。
const ChatRoom: FC<Props> = ({ roomId }) => {
const createOptions = () => {
return {
serverUrl: 'https://localhost:1234',
roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 NG: 依存がレンダーのたびに変わる
};
これを解決するには、エフェクトから呼び出さなければならない関数はuseCallback
で包んでしまえばよいでしょう。useCallback
の依存値roomId
が変わらないかぎり、再レンダー間で関数createOptions
は同じだと保証されるからです。
const ChatRoom: FC<Props> = ({ roomId }) => {
// const createOptions = () => {
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
// }
}, [roomId]); // ✅ OK: 依存値roomIdが変わると更新される
useEffect(() => {
const options = createOptions();
}, [createOptions]); // ✅ OK: メモ化されたcreateOptionsが変わったときのみ更新される
};
もっとも、関数型の依存値を除ければさらに望ましいでしょう。このコード例の場合、関数createOptions
はエフェクトの中に移せます。useCallback
は使わずに済み、コードもシンプルになりました(「リアクティブなオブジェクトや関数をエフェクトの中に含める」参照)。
const ChatRoom: FC<Props> = ({ roomId }) => {
useEffect(() => {
const createOptions = () => { // ✅ OK: useCallbackは使わない
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
const options = createOptions();
}, [roomId]); // ✅ OK: roomIdが変わったときのみ更新される
};
カスタムフックを最適化する
カスタムフックを書く場合、返す関数はすべてuseCallback
でラップしてください。利用する開発者が、必要に応じてコードを最適化できるからです。
const useRouter = () => {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url: string) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
};
トラブルへの対応
コンポーネントがレンダーされるたびにuseCallback
から返される関数が異なる
コンポーネントがレンダーされるたびにuseCallback
から返される関数が異なるという場合は、まず第2引数の依存配列を忘れていないか確かめましょう。第2引数がなければ、useCallback
の戻り値となる関数はレンダーのたびに新たになります。
const ProductPage: FC<Props> = ({ productId, referrer }) => {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 NG: 依存配列が与えられないとつねに新たな関数が返される
};
useCallback
に、依存配列は正しく与えなければなりません。
const ProductPage: FC<Props> = ({ productId, referrer }) => {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
// });
}, [productId, referrer]); // ✅ OK: 依存が変わらないかぎり戻り値の関数は同じ
};
それでもレンダーのたびにuseCallback
から異なる関数が返されるなら、依存値の少なくともひとつは毎回変わっているということです。その値をつきとめなければなりません。コンポーネントから依存値をconsole.log
でコンソールに出力しましょう。
const ProductPage: FC<Props> = ({ productId, referrer }) => {
const handleSubmit = useCallback((orderDetails) => {
}, [productId, referrer]);
console.log([productId, referrer]);
};
異なる再レンダーからコンソールにログ出力された値を右クリックすると、[グローバル変数として保存]が選べます。最初の値がtemp1
、2番目はtemp2
といった具合です。すると、ブラウザコンソールでそれらの値をObject.is
で同じかどうか確かめられます。
// 2回のレンダー間で配列要素が一致しているかを確かめる
Object.is(temp1[0], temp2[0]);
Object.is(temp1[1], temp2[1]);
Object.is(temp1[2], temp2[2]);
メモ化を損なう依存値があったら、除いてください。あるいは、その値をメモ化しましょう。
ループ内のリスト項目からuseCallback
は呼び出せない
リスト項目からループ処理で、複数の子コンポーネントをつくる場合の問題です。以下のReportList
は、配列(items
)からmap
で子コンポーネントChart
を生成しています。Chart
は、memo
で包まれている想定です。子にプロパティ(onClick
)として渡している関数handleClick
はメモ化して、すべての子コンポーネントが無駄に再レンダーされるのを避けたい場合もあるでしょう。
けれど、ループ処理の中でuseCallback
は呼び出せません。フックが使えるのはReact関数のトップレベルのみだからです。
const ReportList: FC<Props> = ({ items }) => {
return (
<article>
{items.map((item) => {
// 🔴 NG: ループの中からuseCallbackは呼び出せない
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
};
この場合、useCallback
をトップレベルで呼び出せるよう、JSXとともに別コンポーネント(Report
)に切り出せばよいでしょう。
const ReportList: FC<Props> = ({ items }) => {
return (
<article>
{items.map((item) =>
<Report key={item.id} item={item} />
)}
</article>
);
};
const Report: FC<Props> = ({ item }) => {
// ✅ OK: useCallbackはトップレベルで呼び出す
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
};
あるいは、今回のコード例でしたら、切り分けた子コンポーネント(Report
)をmemo
でラップしても構いません。渡されたプロパティitem
が変わらなければ、子のChart
コンポーネントも含めて再レンダーが省かれます。関数handleClick
も、item
に依存させるuseCallback
で包む必要がありません。
const Report: FC<Props> = memo(({ item }) => {
const handleClick = () => {
sendReport(item);
}
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});