【React useCallback】パフォーマンスと再レンダリングを最適化するフックの使い方

はじめに

Reactでアプリケーションを作っていると、コンポーネントの再レンダリングが意外と増えてしまうと感じることはないでしょうか。 小さなコンポーネントの集まりでも、何度も再レンダリングされるとパフォーマンスが落ちてしまいます。 開発を進めるうちに、コンポーネント同士の連携や状態管理が複雑になっていき、思わぬタイミングでパフォーマンスの問題に直面することがあります。

こういった場面で役立つのが、useCallback フックです。 useCallbackはReactのフックの一つで、特に再レンダリングの最適化に効果があります。 この記事では、初心者の方でも理解できるように、useCallbackの基本概念から実務での活用例まで、具体的なコードを交えて解説していきます。

この記事を読むとわかること

  • useCallback の基本的な使い方と、どんなメリットがあるのか
  • 再レンダリングが起きる仕組みと、その抑制方法
  • 実務でuseCallbackが活躍する場面の具体例
  • 他のフック(useMemoやuseRefなど)と比較したときの位置づけ
  • 初心者が気をつけたいポイントや、よくあるつまずき

上記の内容を理解することで、Reactアプリケーションのパフォーマンスをより良い状態で保ちながら開発を進めるヒントを得られるでしょう。 コード例も掲載しますので、画面を分割しながら実際に使ってみるのもおすすめです。

React useCallbackとは

Reactには複数のフックが存在しますが、useCallback は「メモ化されたコールバック関数」を返してくれるフックです。 関数コンポーネントで新しい関数を定義すると、レンダリングのたびに毎回新しい関数オブジェクトが生成されます。 これがコンポーネントの再レンダリングを誘発する一因になる場合があり、特に子コンポーネントへコールバックを渡す際に問題が起きやすいです。

使い方の概要

useCallbackの構文はシンプルです。 関数と依存配列を引数に取り、メモ化された関数を返します。 依存配列の値が変わったときに新しい関数を生成し、それ以外のときは前回生成した関数を再利用します。

import React, { useState, useCallback } from "react";

function Example() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={handleClick}>増やす</button>
    </div>
  );
}

export default Example;

上記のように、第二引数の依存配列が空の場合、handleClickはコンポーネントが初回にレンダリングされたときだけ生成されます。 countの更新は関数内で行いますが、依存配列にcountを指定していないため、handleClick自体は再レンダリングごとに再生成されません。 これにより、ボタンを押したとき以外で不要な再レンダリングが起きにくくなります。

useCallbackで何が解決できるのか

一般的に、Reactは仮想DOMを用いた効率的なレンダリングを特徴としています。 しかし、親コンポーネントから子コンポーネントへプロップとして渡している関数がレンダリングのたびに新しく生成されると、子コンポーネント側で「プロップが変わった」と判断される可能性があります。

もし子コンポーネントがReact.memoなどでメモ化していたとしても、コールバック関数が毎回違うものだと認識されると、再レンダリングは防げません。 useCallbackを使えば、依存関係が変化しない限り同じ関数を使い回すため、この問題をある程度解決できます。

再レンダリングをどう抑制するか

コンポーネントの最適化を考えるとき、どんなタイミングで再レンダリングが行われるのかを理解することは重要です。 Reactは「ステートが変わった」「プロップが変わった」といったイベントを契機に再レンダリングを実行します。 useCallbackを利用すると、「プロップとして渡す関数オブジェクト」が無駄に変化する といった要因を抑えられるのです。

たとえば、以下のように子コンポーネントへコールバックを渡すケースを考えてみてください。

function Parent() {
  const [count, setCount] = useState(0);

  // 毎回新しい関数が生成される
  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <div>
      <Child onClick={increment} />
      <p>カウント: {count}</p>
    </div>
  );
}

function Child({ onClick }) {
  console.log("Childコンポーネントが再レンダリングされました");
  return <button onClick={onClick}>子コンポーネントのボタン</button>;
}

上記のParentコンポーネントでは、incrementがParentのレンダリングのたびに新しく作られます。 子コンポーネントにとっては、この関数がプロップとして毎回違うものに見えるため、React.memoを使っていたとしてもChildが再レンダリングされてしまいます。

そこでuseCallbackを用いると、以下のようになります。

function Parent() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <Child onClick={increment} />
      <p>カウント: {count}</p>
    </div>
  );
}

function Child({ onClick }) {
  console.log("Childコンポーネントが再レンダリングされました");
  return <button onClick={onClick}>子コンポーネントのボタン</button>;
}

今度はincrementが常に同じ関数オブジェクトであるため、子コンポーネントの再レンダリング回数が減ることが期待できます。 これがuseCallbackを使う最大の狙いです。

実務での活用シーン

実務においては、画面遷移や複雑な入力フォームなど、多くのコンポーネントが入れ子になるケースがよくあります。 状態管理ライブラリ(Reduxなど)を使用している場合でも、コンポーネント内で書く小さなコールバックが原因で再レンダリングが増えることがあります。 特に、以下のような場面ではuseCallbackが効果を発揮しやすいです。

  1. 子コンポーネントへ頻繁に関数を渡す
  2. 入力フォームで各入力項目ごとにコールバック関数が存在する
  3. APIリクエストを発行する関数を子コンポーネントに渡す

これらのシチュエーションで不要な再レンダリングを起こしてしまうと、パフォーマンスが落ちるだけでなく、思いがけないバグの温床になる場合もあります。 useCallbackを活用することで、こうした問題の防止に一役買ってくれるでしょう。

具体的なコード例

次に、APIリクエストを模したサンプルコードを見てみましょう。 新しいユーザーを登録するようなシンプルなイメージです。

import React, { useState, useCallback } from "react";

function UserRegistration() {
  const [username, setUsername] = useState("");
  const [message, setMessage] = useState("");

  const handleInput = (e) => {
    setUsername(e.target.value);
  };

  const submitUser = useCallback(() => {
    // 実際のAPIリクエストを呼ぶイメージ
    // fetch("/api/register", { method: "POST", body: JSON.stringify({ username }) })
    setMessage(`ユーザー「${username}」を登録しました。`);
  }, [username]);

  return (
    <div>
      <input value={username} onChange={handleInput} placeholder="ユーザー名を入力" />
      <button onClick={submitUser}>登録</button>
      <p>{message}</p>
    </div>
  );
}

export default UserRegistration;

上記の例では、submitUserが依存配列でusernameを指定しているため、ユーザー名が変わるたびに新しい関数が生成されます。 しかし、逆に言えば、ユーザー名が変わらない限りは同じ関数を使い回すということになります。 ユーザー名を入力していない間や、入力がすでに完了したあとは再レンダリングが抑えられるので、多くの入力要素を扱う画面などで快適さを保ちやすくなるわけです。

useCallbackで気をつけたいポイント

useCallbackは便利なフックですが、使う場所を誤ると逆に複雑化する恐れもあります。 また、実行される頻度が高い関数は、依存配列の内容を誤るとバグにつながりやすいので注意が必要です。

依存配列の扱い

依存配列に指定したステートや変数が変わらなければ、useCallbackが返す関数は再生成されません。 一方で、依存配列に入れ忘れた変数があると、思うように動作しない可能性があります。

このような場合、Reactの開発ツール(ESLintなど)で「依存配列に○○を追加してください」という警告が出ることがあります。 もし警告を無視すると、関数が古いステートを参照したままになってしまうなどの問題が起こるかもしれません。

依存配列の指定を誤ると、ステートが更新されないまま関数が動き続けたり、逆に必要ないタイミングで関数が再生成されたりするので注意してください。

他のフックとの比較

パフォーマンス最適化のためには、useCallbackのほかにもいくつかのフックや仕組みが活用できます。 代表的なものに useMemouseRef がありますが、それぞれメモ化する対象や用途が異なります。

以下の表にざっくりとした比較をまとめました。

フックメモ化するもの主な用途
useCallback関数オブジェクト再レンダリング抑制や子コンポーネントへのコールバック最適化
useMemo計算結果の値(変数)計算コストが高い処理の結果をメモ化
useRef値またはDOM要素の参照DOM操作や変数の永続化

useMemoは、たとえば大量の計算や配列のフィルタリングなど、処理コストが高いロジックをメモ化して、不要な再計算を回避するために使われます。 一方でuseRefは、DOM要素へ直接アクセスしたり、再レンダリング間で保持したい値を格納するのに便利です。

いずれもReactアプリケーションのパフォーマンスや管理のしやすさを向上させる武器です。 特定の場面でどれが最適かを判断するには、実際のロジックやデータ量を見極める必要があります。

まとめ

Reactでコンポーネントを作るときは、データの流れや状態の管理に意識を向けるだけで手一杯になりがちです。 ですが、パフォーマンス面もしっかり考えないと、後から修正が難しくなることがあります。 useCallback は、そんなときに「余計な再レンダリングを防いでくれる」という観点から有用です。

ただし、何でもかんでもuseCallbackで包めばいいわけではありません。 依存配列の管理をはじめとする細かい注意点を踏まえながら、必要な箇所だけに絞って使うのがコツです。 さらに、useMemoやuseRefといった他のフックの知識も組み合わせると、Reactアプリケーション全体をより適切に最適化できるでしょう。

皆さんも、実際にコードを書きながら使いどころを探ってみてください。 入力フォームやダッシュボードなど、要素が増えれば増えるほど再レンダリングの影響は大きくなります。 うまくuseCallbackを取り入れることで、よりスムーズな開発体験を得られるはずです。

Reactをマスターしよう

この記事で学んだReactの知識をさらに伸ばしませんか?
Udemyには、現場ですぐ使えるスキルを身につけられる実践的な講座が揃っています。