1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ta1m1kamが一人で書くAdvent Calendar 2022

Day 20

ReactHooksについてまとめる(Performance Hooks useCallback)

Last updated at Posted at 2022-12-19

概要

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化されています。

ShippingForm.jsx
import { memo } from 'react';

const ShippingForm = memo(function ShippingForm({ onSubmit }) {
  // ...
});

このShippingFormコンポーネントが親コンポーネントで使用され、propsとして、上記の例で使用したuseCallbackでラップされたhandleSubmit関数を取るようにすると再レンダリングの際に依存するprops(productId, referrer)に変更がない限り再レンダリングをスキップするように動作します。

ProductPage.jsx
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,
  };
}

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?