概要
ReactHooksのPerformance Hooksについてのメモ記事です。基本的にReactの公式ドキュメントの咀嚼記事となっています。
今回はuseCallback編です。
useCallback
コンポーネントの再レンダリングスキップ
レンダリングパフォーマンスを最適化する際に、子コンポーネントに渡す関数をキャッシュする必要がある場合があります。まず、キャッシュを行うためにコンポーネントの関数をuseCallbackでラップします。
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
useCallbackには引数が2つあります。
1つ目の引数には、再レンダリングの際にキャッシュしたい関数定義。2つ目は関数内で使用されるコンポーネント内の依存変数の配列です。
再レンダリング時に、依存配列の値を比較して変更されてない場合は以前と同じ関数を返し、変更されている場合は更新された関数を返します。
これは子コンポーネントの再レンダリングの際に効果を発揮します。例えば以下のようなShippingForm
コンポーネントがあり、関数を引数として受け取ります。このコンポーネントはmemo化されています。
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
このShippingForm
コンポーネントが親コンポーネントで使用され、propsとして、上記の例で使用したuseCallbackでラップされたhandleSubmit
関数を取るようにすると再レンダリングの際に依存するprops(productId, referrer)に変更がない限り再レンダリングをスキップするように動作します。
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
メモ化されたコールバックからStateを更新する
メモ化されたコールバックの関数内でStateを更新する必要がある場合に付いてです。
handleAddTo関数で新しいtodoを追加しています。
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
こういう場合には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
// ...
Effectが頻繁に発火しないようにする
useEffect内で関数を呼び出したい場合があります。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // ← This function
const connection = createConnection();
connection.connect();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problem: This dependency changes on every render
この場合、createOptions関数を依存配列に入れてしまうと、毎回Effectが発火してしまいます。
これを解決するには、呼び出されている createOptions()
を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
// ...
これでChatRoomコンポーネントのpropsであるroomId
が変更されない限りスキップされます。
以下のように関数自体をuseEffect内に入れてしまえば、依存関係が1つ減らすことができます。
こうすることで、useCallback自体が不要となることもあります。
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
// ...
カスタムHooksを最適化する
カスタムHooksを書く場合、その関数を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,
};
}
参考