0
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?

React Hooks をHackしよう!【Part20: useStateやuseEffectのみ使っていませんか?useState・useReducer・useRef・useContext・useEffect・useMemo・useCallbackを適切に使ってみよう!】

Posted at

React 開発をしていて、こんな経験はありませんか?

// とりあえず useState と useEffect で書いちゃう...
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  fetchData().then(setData).catch(setError).finally(() => setLoading(false));
}, []);

「useState と useEffect さえあれば、なんとかなる!」

……それ、本当に正しい選択ですか? 🤔

実は React には、場面に応じて使い分けることができる Hooks がいろいろあります。適切に選べば、コードがスッキリし、パフォーマンスも向上し、バグも減ります。

本記事では、useState・useReducer・useRef・useContext・useEffect・useMemo・useCallback の 7 つを徹底比較。「いつ」「なぜ」「どう使い分けるか」を、コード例・比較表・フローチャートで分かりやすく解説します!

📝 検証環境: この記事は facebook/react リポジトリ(2025年12月時点の main ブランチ)の実際のソースコードを参照して書かれています。

標準Hooksのみではなく外部ライブラリを使うという選択肢ももちろんありますのでライブラリの検討もしてみましょう

1. 7 つの Hook 概要比較

まずは一覧で全体像を把握しましょう。それぞれがどんな目的で使われ、何を返すのかを理解することで、適切な Hook を選ぶ第一歩になります。

Hook 目的 返り値 主な使用場面
useState 状態の保持・更新 [state, setState] 単純な値の管理(カウンター、入力値、フラグ)
useReducer 複雑な状態遷移の管理 [state, dispatch] 関連する複数の状態、複雑な更新ロジック
useRef 再レンダーなしで値を保持 { current: value } DOM参照、タイマーID、前回値の保持
useContext Context 値の読み取り Context の現在値 Props drilling の回避、グローバルデータ共有
useEffect 副作用の実行 void API呼び出し、DOM操作、イベントリスナー登録
useMemo 計算結果のメモ化 メモ化された値 重い計算のキャッシュ、参照の安定化
useCallback 関数のメモ化 メモ化された関数 子コンポーネントへの関数渡し、依存配列の安定化

Hook の分類

2. useState — 状態管理の基本

2.1 役割

useStateコンポーネントに状態を持たせ、状態変更時に再レンダーをトリガー するための Hook です。

React のコンポーネントは純粋関数として設計されていますが、ユーザーインタラクションに応じて UI を変化させるには「状態」が必要です。ローカル変数では React に変更を通知できないため、useState を使って状態を React に管理してもらいます。

2.2 基本的な使い方

import { useState } from 'react';

function Counter() {
  // 初期値 0 で状態を作成
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>カウント: {count}</p>
      {/* 直接値を設定 */}
      <button onClick={() => setCount(0)}>リセット</button>
      {/* 更新関数で前の値を参照 */}
      <button onClick={() => setCount((prev) => prev + 1)}>+1</button>
    </div>
  );
}

2.3 Hookの使用

useState の引数と返り値を整理します。

const [state, setState] = useState(initialState);
引数/返り値 説明
initialState 状態の初期値。関数を渡すと遅延初期化される
state 現在の状態値
setState 状態を更新する関数。値または更新関数を受け取る

2.4 重要なポイント

❌ ローカル変数では再レンダーされない

// ❌ これはうまくいかない
function Counter() {
  let count = 0;
  const increment = () => {
    count++; // 変更しても React は知らない
  };
  return <button onClick={increment}>{count}</button>;
}

✅ useState を使えば再レンダーされる

// ✅ 正しい方法
function Counter() {
  const [count, setCount] = useState(0);
  const increment = () => {
    setCount((c) => c + 1); // React に変更を通知
  };
  return <button onClick={increment}>{count}</button>;
}

更新関数形式を使うべき場面

連続した更新や非同期処理では、更新関数形式を使いましょう:

// ❌ 同じ値で上書きされる可能性
setCount(count + 1);
setCount(count + 1);
// → 結果: +1 のみ

// ✅ 安全に連続更新
setCount((c) => c + 1);
setCount((c) => c + 1);
// → 結果: +2

2.5 facebook/react での実装ポイント

facebook/react リポジトリを確認すると、useState は内部的に useReducer の特殊ケースとして実装されています:

// packages/react/src/ReactHooks.js
export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

内部では basicStateReducer という単純な reducer が使用されています:

// packages/react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

つまり、useState は「シンプルな reducer を持つ useReducer」 と言えます。

3. useReducer — 複雑な状態遷移の管理

3.1 役割

useReducer複数の関連する状態や複雑な更新ロジックを一元管理 するための Hook です。Redux と同じ Reducer パターンを採用しています。

3.2 基本的な使い方

import { useReducer } from 'react';

type State = {
  count: number;
  step: number;
};

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'SET_STEP'; step: number }
  | { type: 'RESET' };

const initialState: State = { count: 0, step: 1 };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + state.step };
    case 'DECREMENT':
      return { ...state, count: state.count - state.step };
    case 'SET_STEP':
      return { ...state, step: action.step };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>カウント: {state.count}(ステップ: {state.step}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'SET_STEP', step: 5 })}>
        ステップを5に
      </button>
      <button onClick={() => dispatch({ type: 'RESET' })}>リセット</button>
    </div>
  );
}

3.3 Hookの使用

const [state, dispatch] = useReducer(reducer, initialArg, init?);
引数/返り値 説明
reducer (state, action) => newState の純粋関数
initialArg 初期状態の計算に使う値
init (オプション) 遅延初期化関数 (initialArg) => initialState
state 現在の状態
dispatch アクションを送信する関数

3.4 useState vs useReducer の使い分け

観点 useState useReducer
状態の数 1つの独立した値 複数の関連する値
更新ロジック シンプル 複雑・条件分岐が多い
テスト コンポーネントテスト reducer を単体テスト可能
コード量 少ない やや多い
デバッグ 直接的 action の履歴を追跡可能

3.5 useReducer を選ぶべきケース

// ❌ useState だと管理が煩雑
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);

// 更新ロジックが散らばる
const handleSubmit = async () => {
  setIsSubmitting(true);
  setError(null);
  setSuccess(false);
  try {
    await submitForm({ name, email });
    setSuccess(true);
  } catch (e) {
    setError(e.message);
  } finally {
    setIsSubmitting(false);
  }
};
// ✅ useReducer で一元管理
type State = {
  name: string;
  email: string;
  isSubmitting: boolean;
  error: string | null;
  success: boolean;
};

type Action =
  | { type: 'SET_FIELD'; field: 'name' | 'email'; value: string }
  | { type: 'SUBMIT_START' }
  | { type: 'SUBMIT_SUCCESS' }
  | { type: 'SUBMIT_ERROR'; error: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'SET_FIELD':
      return { ...state, [action.field]: action.value };
    case 'SUBMIT_START':
      return { ...state, isSubmitting: true, error: null, success: false };
    case 'SUBMIT_SUCCESS':
      return { ...state, isSubmitting: false, success: true };
    case 'SUBMIT_ERROR':
      return { ...state, isSubmitting: false, error: action.error };
    default:
      return state;
  }
}

💡 外部ライブラリで状態管理を行う選択肢も

  • Redux
    Reduxは広く使われている状態管理ライブラリで、複雑なアプリケーションに適している
  • Zustand
    軽量でシンプルな状態管理ライブラリで、学習コストが低い

詳しくは

https://qiita.com/JavaLangRuntimeException/items/7da82ea3a2504f7325f0


4. useRef — 再レンダーなしで値を保持

useState は値の変更で再レンダーをトリガーしますが、再レンダーが不要な値を保持したい場合もあります。useRef は再レンダーを引き起こさずに値を保持でき、DOM 要素への参照にも使えます。

4.1 役割

useRef再レンダーをトリガーせずに値を保持 するための Hook です。また、DOM 要素への参照 を取得する用途にも使われます。

💡 useRef の2つの用途

  1. 値の保持: タイマーID、前回の値など、再レンダー不要な値を保持
  2. DOM の参照: <input ref={inputRef} /> のように DOM 要素に直接アクセス

4.2 useState との違い

useRefuseState は値を保持する点では似ていますが、再レンダーの有無という重要な違いがあります。

観点 useState useRef
値の変更 setState で変更 .current に直接代入
再レンダー 変更すると再レンダー する 変更しても再レンダー しない
用途 画面に表示する値 画面に影響しない値、DOM 参照
function Timer() {
  // ❌ useState: intervalId の変更で不要な再レンダーが発生
  const [intervalId, setIntervalId] = useState(null);
  
  const start = () => {
    const id = setInterval(() => console.log('tick'), 1000);
    setIntervalId(id);  // 再レンダーが発生!(不要)
  };

  // ✅ useRef: 再レンダーなしで値を保持
  const intervalRef = useRef(null);
  
  const startWithRef = () => {
    intervalRef.current = setInterval(() => console.log('tick'), 1000);
    // 再レンダーは発生しない
  };
}

💡 使い分けの基準

  • 画面に表示される値useState
  • 画面に表示されない値(タイマーID、前回値など)→ useRef

4.3 基本的な使い方

用途1: 値の保持

import { useRef, useState, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);  // レンダー回数を記録

  // レンダーごとにカウントアップ(再レンダーは発生しない)
  renderCount.current += 1;

  return (
    <div>
      <p>Count: {count}</p>
      <p>Render count: {renderCount.current}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

用途2: DOM の参照

import { useRef } from 'react';

function TextInput() {
  const inputRef = useRef(null);

  const handleClick = () => {
    // DOM メソッドを直接呼び出し
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>フォーカス</button>
    </div>
  );
}

4.4 Hookの使用

useRef の引数と返り値を整理します。

const ref = useRef(initialValue);
引数/返り値 説明
initialValue ref の初期値(初回レンダー時のみ使用)
ref { current: initialValue } の形をしたオブジェクト

💡 初期値は初回のみ使用される
useRef(initialValue)initialValue は、初回レンダー時にのみ使用されます。2回目以降のレンダーでは無視され、前回の値が保持されます。

4.5 useRef の3つの特徴

特徴 説明
レンダー間で値を保持 通常の変数はレンダーごとにリセットされるが、ref は保持される
変更しても再レンダーしない ref.current を変更しても React は再レンダーをトリガーしない
ミュータブル state と違い、ref.current は直接書き換え可能

4.6 よくあるユースケース

1. タイマーの管理

function Stopwatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);

  const start = () => {
    if (intervalRef.current !== null) return; // 二重起動防止
    intervalRef.current = setInterval(() => {
      setTime((t) => t + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <div>
      <p>{time}</p>
      <button onClick={start}>開始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

2. 前回の値を記憶

function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(0);

  useEffect(() => {
    prevCountRef.current = count;  // レンダー後に前回値を保存
  });

  return (
    <div>
      <p>現在: {count}, 前回: {prevCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

💡 なぜ再レンダーされないのか?
ref はただの JavaScript オブジェクト { current: value } です。ref.current = newValue はただのプロパティ代入であり、React に何も通知しません。一方、setState は内部で dispatchSetState を呼び、再レンダーをスケジュールします。

4.7 注意点

❌ ref.current を画面表示に使わない

// ❌ 間違い: ref の値は画面に反映されない
function Counter() {
  const countRef = useRef(0);
  
  const increment = () => {
    countRef.current += 1;  // 再レンダーされないので画面は更新されない!
  };

  return <button onClick={increment}>{countRef.current}</button>;
}

// ✅ 正しい: 画面に表示する値は useState を使う
function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount((c) => c + 1);
  };

  return <button onClick={increment}>{count}</button>;
}

5. useContext — コンポーネントツリー全体でのデータ共有

深くネストしたコンポーネントにデータを渡すとき、props を何層もバケツリレーするのは面倒です。useContext を使えば、中間コンポーネントを経由せずに、必要なコンポーネントで直接データを取得できます。

5.1 役割

useContextContext の値を読み取り、変更を購読 するための Hook です。Props drilling(バケツリレー)を解消し、ツリー全体でデータを共有できます。

💡 Context とは?
Context は、コンポーネントツリー全体でデータを共有するための React の仕組みです。テーマ、認証情報、言語設定など、多くのコンポーネントで必要なデータを効率的に渡すことができます。

5.2 Props drilling(バケツリレー)問題

Context を理解するために、まず Props drilling の問題を見てみましょう。Props drilling とは、深くネストしたコンポーネントにデータを渡すために、中間のすべてのコンポーネントを経由して props を渡す必要がある状態のことです。

❌ Props drilling(バケツリレー)の例

中間コンポーネントが使わない props を受け取って渡すだけの状態になり、コードが冗長になります。

function App() {
  const [user, setUser] = useState(null);
  return <Header user={user} />;
}

function Header({ user }) {
  return <Navigation user={user} />;  // 使わないのに受け取る
}

function Navigation({ user }) {
  return <UserMenu user={user} />;    // 使わないのに受け取る
}

function UserMenu({ user }) {
  return <span>{user?.name}</span>;   // ここで初めて使う
}

✅ Context による解決

Context を使えば、中間コンポーネントは props を受け取る必要がなく、必要な場所で直接データを取得できます。

const UserContext = createContext(null);

function App() {
  const [user, setUser] = useState(null);
  return (
    <UserContext value={user}>
      <Header />
    </UserContext>
  );
}

function Header() {
  return <Navigation />;  // user を渡す必要なし
}

function Navigation() {
  return <UserMenu />;    // user を渡す必要なし
}

function UserMenu() {
  const user = useContext(UserContext);  // 直接取得
  return <span>{user?.name}</span>;
}

5.3 基本的な使い方

Context を使うには、3 つのステップを踏みます:(1) createContext で Context を作成、(2) Provider で値を提供、(3) useContext で値を取得。以下の例で流れを確認しましょう。

import { createContext, useContext, useState } from 'react';

// 1. Context を作成
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');

// 2. Provider で値を提供
function App() {
  const [theme, setTheme] = useState<Theme>('light');

  return (
    <ThemeContext value={theme}>
      <Header />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        テーマ切り替え
      </button>
    </ThemeContext>
  );
}

// 3. 中間コンポーネント(theme を知らなくてOK)
function Header() {
  return <Navigation />;
}

function Navigation() {
  return <ThemeButton />;
}

// 4. useContext で値を取得
function ThemeButton() {
  const theme = useContext(ThemeContext);
  return (
    <button className={`btn-${theme}`}>
      現在のテーマ: {theme}
    </button>
  );
}

5.4 Hookの使用

useContext の引数と返り値を整理します。

const value = useContext(SomeContext);
引数/返り値 説明
SomeContext createContext で作成した Context オブジェクト
value ツリー内で最も近い Provider の値(なければデフォルト値)

💡 Provider が見つからない場合
ツリー内に対応する Provider がない場合、createContext に渡したデフォルト値が返されます。これはテスト時やコンポーネントを単体で使用する場合に便利です。

5.5 useContext の適切な使用場面

Context は便利ですが、すべての場面で使うべきではありません。以下の表を参考に、適切な場面を判断しましょう。

✅ 適している ❌ 適していない
テーマ設定 頻繁に変更される値
認証情報(ユーザー) 細粒度の状態管理
言語・ロケール パフォーマンスが重要な場面
設定値 1〜2階層の props 渡し

5.6 注意点:再レンダーの範囲

Context を使う際の最大の注意点は パフォーマンスへの影響 です。Context の値が変わると、その Context を使用しているすべてのコンポーネントが再レンダーされます。

❌ 毎回新しいオブジェクトを渡す問題

// ❌ value が変わるとすべての Consumer が再レンダー
function App() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  // オブジェクトを毎回生成 → 毎回異なる参照
  return (
    <AppContext value={{ user, theme }}>
      <Content />
    </AppContext>
  );
}

✅ useMemo で参照を安定化

useMemo を使って Context の値をメモ化することで、不要な再レンダーを防げます。

// ✅ useMemo で参照を安定化
function App() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  const value = useMemo(() => ({ user, theme }), [user, theme]);

  return (
    <AppContext value={value}>
      <Content />
    </AppContext>
  );
}

💡 Context を分割するテクニック
頻繁に変わる値と、あまり変わらない値を同じ Context に入れると、すべての Consumer が再レンダーされてしまいます。このような場合は、Context を分割することを検討しましょう。例えば UserContextThemeContext を別々に作成すると、テーマが変わってもユーザー情報を使うコンポーネントは再レンダーされません。

6. useEffect — 副作用の実行

6.1 役割

useEffectレンダリング後に副作用を実行 するための Hook です。React コンポーネントは純粋関数であるべきですが、実際のアプリでは外部システムとの連携が必要です。

6.2 「副作用」とは?

副作用(Side Effect)とは、コンポーネントのレンダリング(UI の計算)以外の処理を指します。以下のような操作が該当します:

  • ネットワークリクエスト: APIからのデータ取得
  • DOM 操作: 要素のサイズ測定、フォーカス管理
  • サブスクリプション: WebSocket、イベントリスナー
  • タイマー: setTimeout, setInterval

💡 なぜ「副作用」と呼ぶのか?
純粋関数は「入力 → 出力」の計算のみを行い、外部に影響を与えません。それ以外の「外部への影響」や「外部からの影響を受ける処理」を副作用と呼びます。React はレンダリングを純粋に保つために、副作用を useEffect に分離します。

6.3 基本的な使い方

useEffect は第1引数にセットアップ関数、第2引数に依存配列を受け取ります。セットアップ関数からクリーンアップ関数を返すことで、コンポーネントのアンマウント時や再実行前に後片付けができます。

import { useState, useEffect } from 'react';

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // セットアップ: 接続を開始
    const connection = createConnection(roomId);
    connection.connect();
    connection.on('message', (msg) => {
      setMessages((prev) => [...prev, msg]);
    });

    // クリーンアップ: 接続を解除
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // 依存配列: roomId が変わったら再実行

  return <MessageList messages={messages} />;
}

6.4 Hookの使用

useEffect の引数を整理します。

useEffect(setup, dependencies)
引数 説明
setup 副作用のロジックを含む関数。クリーンアップ関数を返せる
dependencies (オプション) 依存配列。この値が変わった時にエフェクトが再実行

セットアップ関数とクリーンアップ関数の関係

useEffect に渡す関数(セットアップ関数)と、その戻り値(クリーンアップ関数)の関係を理解しましょう。

useEffect(() => {
  // ここがセットアップ関数の本体
  const connection = createConnection(roomId);
  connection.connect();
  
  // 戻り値がクリーンアップ関数
  return () => {
    connection.disconnect();
  };
}, [roomId]);
項目 説明 React内部での名前
セットアップ関数 useEffect に渡す第1引数の関数 create
クリーンアップ関数 セットアップ関数の 戻り値 destroy

💡 facebook/react ソースコードでの実装

packages/react-reconciler/src/ReactFiberHooks.js で Effect 型が定義されています:

export type Effect = {
  tag: HookFlags,
  inst: EffectInstance,        // クリーンアップ関数を保持
  create: () => (() => void) | void,  // セットアップ関数
  deps: Array<mixed> | void | null,   // 依存配列
  next: Effect,
};

type EffectInstance = {
  destroy: void | (() => void),  // クリーンアップ関数
};

create(セットアップ関数)を実行した戻り値が destroy(クリーンアップ関数)として inst.destroy に保存されます。

6.5 依存配列のパターン

依存配列の指定方法によって、エフェクトの実行タイミングが変わります。3 つのパターンを理解しましょう。

// パターン1: 依存配列あり → 依存値が変わった時のみ再実行
useEffect(() => {
  fetchData(userId);
}, [userId]);

// パターン2: 空の依存配列 → マウント時のみ実行
useEffect(() => {
  initializeAnalytics();
  return () => cleanupAnalytics();
}, []);

// パターン3: 依存配列なし → 毎回のレンダー後に実行(通常は避ける)
useEffect(() => {
  document.title = `Count: ${count}`;
});

💡 依存配列の選び方

  • 特定の値が変わったときだけ実行したい → その値を依存配列に含める
  • マウント時に一度だけ実行したい → 空の配列 []
  • 毎回実行したい → 依存配列を省略(ただし、ほとんどの場合は避けるべき)

マウントとは?
コンポーネントが初めて DOM に追加されることを指します。useEffect の依存配列を空にすると、コンポーネントのライフサイクルの中で「マウント時」に一度だけエフェクトが実行されます。

アンマウントとは
コンポーネントが DOM から削除されることを指します。useEffect のクリーンアップ関数は、コンポーネントがアンマウントされる際に実行され、リソースの解放やイベントリスナーの解除などに使用されます。

6.6 クリーンアップが必要なケース

副作用によっては、コンポーネントがアンマウントされる時や、エフェクトが再実行される前に「後片付け」が必要です。クリーンアップを忘れるとメモリリークの原因になります。

副作用の種類 セットアップ クリーンアップ
イベントリスナー addEventListener removeEventListener
WebSocket new WebSocket() socket.close()
setInterval setInterval() clearInterval()
サブスクリプション subscribe() unsubscribe()

以下はウィンドウサイズを監視する例です。クリーンアップでイベントリスナーを解除しています。

function WindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };

    // セットアップ
    handleResize(); // 初期値を設定
    window.addEventListener('resize', handleResize);

    // クリーンアップ(メモリリーク防止)
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <p>{size.width} x {size.height}</p>;
}

6.7 useEffect を使うべきでないケース

useEffect は万能ではありません。レンダー中に計算できるものは、useEffect を使わずに直接計算すべきです。不要な useEffect は、コードを複雑にし、パフォーマンスを悪化させます。

// ❌ レンダー中に計算できるものは useEffect 不要
function FilteredList({ items, query }) {
  const [filteredItems, setFilteredItems] = useState([]);

  useEffect(() => {
    setFilteredItems(items.filter((item) => item.includes(query)));
  }, [items, query]);

  return <List items={filteredItems} />;
}

// ✅ レンダー中に計算
function FilteredList({ items, query }) {
  const filteredItems = items.filter((item) => item.includes(query));
  return <List items={filteredItems} />;
}

7. useMemo — 計算結果のメモ化

React では再レンダーのたびにコンポーネント関数が再実行されます。重い計算を毎回実行するとパフォーマンスが低下します。useMemo を使えば、依存値が変わらない限り計算結果を再利用できます。

7.1 役割

useMemo計算結果をキャッシュ し、依存値が変わらない限り再計算をスキップするための Hook です。

💡 メモ化(Memoization)とは?
メモ化とは、関数の計算結果をキャッシュし、同じ入力に対して再計算を省略する最適化テクニックです。useMemo という名前はこの概念に由来しています。

7.2 useMemo と useState の違い

「値を保持する」という点で似ていますが、目的がまったく異なります。

観点 useState useMemo
目的 状態を保持し、変更時に再レンダーをトリガー 計算結果をキャッシュし、不要な再計算を防ぐ
値の変更 setState明示的に 変更 依存配列の値が変わると 自動で 再計算
再レンダー 値が変わると再レンダーを 引き起こす 再レンダーを 引き起こさない(最適化専用)
用途 ユーザー入力、APIレスポンスなど「変わる値」 既存の値から「派生する計算結果」
function Example({ items }) {
  // ❌ useState を派生データに使うのは間違い
  const [sortedItems, setSortedItems] = useState([]);
  useEffect(() => {
    setSortedItems([...items].sort());
  }, [items]);

  // ✅ useMemo を使う(ある値をsortをするといった派生データなので)
  const sortedItems = useMemo(() => {
    return [...items].sort();
  }, [items]);
}

7.3 useMemo と useRef の違い

useMemouseRef はどちらも再レンダーを引き起こしませんが、値の更新タイミングが異なります。

観点 useRef useMemo
目的 任意のタイミングで値を保持 依存値から計算結果をキャッシュ
値の変更 .current手動で 代入 依存配列の値が変わると 自動で 再計算
再レンダー 引き起こさない 引き起こさない
用途 タイマーID、DOM参照、前回値 派生データ、参照の安定化
function Example({ userId }) {
  // useRef: 手動で値を更新(前回のuserIdを保持)
  const prevUserIdRef = useRef(userId);
  useEffect(() => {
    prevUserIdRef.current = userId;
  });

  // useMemo: 依存値から自動計算(userIdが変わったら再計算)
  const userConfig = useMemo(() => {
    return { id: userId, timestamp: Date.now() };
  }, [userId]);
}

💡 使い分けの基準

  • ユーザーが値を変えるuseState
  • 既存の状態から計算で導出できるuseMemo(または直接計算)
  • 再レンダー間で値を保持したいが、画面に表示しないuseRef

何でもかんでもメモ化すれば良いというわけではありません。軽い計算に useMemo を使うと、かえってオーバーヘッドが増えることがあります。

7.4 基本的な使い方

useMemo は第1引数に計算関数、第2引数に依存配列を受け取ります。依存配列の値が変わらない限り、前回の計算結果が再利用されます。

import { useMemo } from 'react';

function TodoList({ todos, tab, theme }) {
  // ✅ todos か tab が変わった時だけ再計算
  const visibleTodos = useMemo(() => {
    return filterTodos(todos, tab);
  }, [todos, tab]);

  return (
    <div className={theme}>
      {visibleTodos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </div>
  );
}

7.5 Hookの使用

useMemo の引数と返り値を整理します。

const cachedValue = useMemo(calculateValue, dependencies);
引数/返り値 説明
calculateValue キャッシュしたい値を計算する純関数
dependencies 計算に使用されるすべてのリアクティブ値の配列
cachedValue メモ化された計算結果

7.6 useMemo を使うべきケース

useMemo が効果を発揮するのは、主に以下の2つの場面です。フローチャートで判断してみましょう。

1. 重い計算のキャッシュ

const sortedItems = useMemo(() => {
  // 1000件以上のデータをソート
  return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

2. 子コンポーネントへのオブジェクト渡し

function Parent({ data }) {
  // ✅ data が変わらなければ同じ参照
  const config = useMemo(() => ({
    color: data.color,
    size: data.size,
  }), [data.color, data.size]);

  return <MemoizedChild config={config} />;
}

7.7 useMemo を使うべきでないケース

単純な計算に useMemo を使うと、かえってオーバーヘッドが増えます。メモ化自体にもコストがあるため、計算が軽い場合は直接計算した方が効率的です。

// ❌ 単純な計算には不要
const doubled = useMemo(() => count * 2, [count]);

// ✅ そのまま計算
const doubled = count * 2;

8. useCallback — 関数のメモ化

useMemo が「値」をメモ化するのに対し、useCallback は「関数」をメモ化します。特に React.memo でラップした子コンポーネントにコールバック関数を渡す場合に重要になります。

8.1 役割

useCallback関数定義をキャッシュ し、依存値が変わらない限り同じ関数参照を保持するための Hook です。

💡 なぜ関数の参照が重要なのか?
JavaScript では () => {} のような関数リテラルは、評価されるたびに新しいオブジェクトを生成します。React.memo は props の参照が変わると再レンダーするため、関数の参照を安定させることが重要になります。

8.2 基本的な使い方

useCallbackReact.memo と組み合わせて使うことで効果を発揮します。以下の例では、productId が変わらない限り、同じ handleClick 関数が再利用されます。

import { useCallback, memo } from 'react';

// memo でラップされた子コンポーネント
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
  console.log('ExpensiveChild レンダー');
  return <button onClick={onClick}>クリック</button>;
});

function Parent({ productId }) {
  // ✅ productId が変わった時だけ新しい関数を生成
  const handleClick = useCallback(() => {
    console.log('Clicked product:', productId);
  }, [productId]);

  return <ExpensiveChild onClick={handleClick} />;
}

8.3 Hookの使用

useCallback の引数と返り値を整理します。

const cachedFn = useCallback(fn, dependencies);
引数/返り値 説明
fn キャッシュしたい関数
dependencies 関数内で参照されるすべてのリアクティブ値の配列
cachedFn メモ化された関数

8.4 なぜ useCallback が必要か

JavaScript の関数とオブジェクトの比較がどのように動作するかを理解すると、useCallback の必要性がわかります。

JavaScript では、関数は毎回新しいオブジェクトとして生成されます:

// 毎回 false
console.log(() => {} === () => {}); // false

// オブジェクトも同様
console.log({} === {}); // false

React.memo でラップしたコンポーネントに関数を渡す場合、毎回新しい関数が渡されると最適化が効きません:

// ❌ theme が変わるたびに handleClick は新しい関数
function Parent({ theme }) {
  const handleClick = () => console.log('click');

  return (
    <div className={theme}>
      {/* memo の最適化が効かない */}
      <MemoizedChild onClick={handleClick} />
    </div>
  );
}

8.5 useMemo vs useCallback

useMemouseCallback はよく似ていますが、メモ化する対象が異なります。どちらを使うか迷ったら、以下の表を参考にしてください。

項目 useMemo useCallback
目的 をメモ化 関数をメモ化
返り値 計算結果(任意の型) 関数
使用場面 重い計算、オブジェクト参照の安定化 子コンポーネントへのコールバック

実は useCallbackuseMemo の特殊ケースです:

// この2つは同等
const handleClick = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

const handleClick = useMemo(() => {
  return () => {
    doSomething(a, b);
  };
}, [a, b]);

💡 どちらを使うべき?
関数をメモ化したい場合は useCallback を使いましょう。useMemo で関数を返すこともできますが、useCallback の方が意図が明確で読みやすいコードになります。

8.6 値を保持する Hooks の総合比較

ここで、値や関数を保持する Hooks を総合的に比較してみましょう。

Hook 保持するもの 変更方法 再レンダー 主な用途
useState 状態 setState で明示的 トリガーする UI に反映する値
useRef 可変参照 .current に直接代入 しない DOM 参照、前回値の保持
useMemo 計算結果 依存配列の変更で自動 しない 重い計算のキャッシュ
useCallback 関数 依存配列の変更で自動 しない memo 化した子への関数渡し
function Example({ userId }) {
  // useState: 変更すると再レンダー
  const [count, setCount] = useState(0);

  // useRef: 変更しても再レンダーしない
  const renderCount = useRef(0);
  renderCount.current += 1;

  // useMemo: userId が変わったら再計算
  const userConfig = useMemo(() => ({ id: userId }), [userId]);

  // useCallback: userId が変わったら新しい関数
  const fetchUser = useCallback(() => {
    return api.getUser(userId);
  }, [userId]);

  return <Child config={userConfig} onFetch={fetchUser} />;
}

💡 選択の基準

  • 値が変わったら画面を更新したいuseState
  • 値を保持したいが画面更新は不要useRef
  • 計算結果をキャッシュしたいuseMemo
  • 関数をキャッシュしたいuseCallback

8.7 facebook/react での実装

React のソースコードを見ると、useCallbackuseMemo は非常によく似た実装になっています。どちらも dispatcher を通じて処理されます。

// packages/react/src/ReactHooks.js
export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}

9. Hook の組み合わせパターン

ここまで各 Hook を個別に学んできましたが、実際のアプリケーションでは複数の Hook を組み合わせて使用します。よくあるパターンを 3 つ紹介します。

パターン1: useReducer + useContext(グローバル状態管理)

Redux を使わずに、React の組み込み機能だけでグローバル状態管理を実現するパターンです。useReducer で状態管理ロジックを定義し、useContext でアプリ全体に共有します。

// StateProvider.tsx
import { createContext, useContext, useReducer, ReactNode } from 'react';

type State = { user: User | null; theme: 'light' | 'dark' };
type Action =
  | { type: 'SET_USER'; user: User }
  | { type: 'LOGOUT' }
  | { type: 'TOGGLE_THEME' };

const StateContext = createContext<State | null>(null);
const DispatchContext = createContext<Dispatch<Action> | null>(null);

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.user };
    case 'LOGOUT':
      return { ...state, user: null };
    case 'TOGGLE_THEME':
      return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
    default:
      return state;
  }
}

export function StateProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(reducer, { user: null, theme: 'light' });

  return (
    <StateContext value={state}>
      <DispatchContext value={dispatch}>
        {children}
      </DispatchContext>
    </StateContext>
  );
}

// カスタムフック
export function useAppState() {
  const context = useContext(StateContext);
  if (!context) throw new Error('StateProvider が必要です');
  return context;
}

export function useAppDispatch() {
  const context = useContext(DispatchContext);
  if (!context) throw new Error('StateProvider が必要です');
  return context;
}

パターン2: useEffect + useState(データフェッチ)

API からデータを取得するための定番パターンです。useState でデータ・ローディング・エラーの状態を管理し、useEffect でフェッチ処理を実行します。カスタムフックとして抽出すると再利用性が高まります。

function useUserData(userId: string) {
  const [data, setData] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(`/api/users/${userId}`);
        const user = await response.json();

        if (!cancelled) {
          setData(user);
        }
      } catch (e) {
        if (!cancelled) {
          setError(e as Error);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      cancelled = true;
    };
  }, [userId]);

  return { data, loading, error };
}

パターン3: useMemo + useCallback(パフォーマンス最適化)

検索可能なリストのように、フィルタリング処理と選択ハンドラを持つコンポーネントでは、useMemouseCallback を組み合わせてパフォーマンスを最適化します。

function SearchableList({ items, onSelect }) {
  const [query, setQuery] = useState('');

  // 重いフィルタリング処理をメモ化
  const filteredItems = useMemo(() => {
    return items.filter((item) =>
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }, [items, query]);

  // 選択ハンドラをメモ化
  const handleSelect = useCallback(
    (item) => {
      onSelect(item);
    },
    [onSelect]
  );

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="検索..."
      />
      <VirtualizedList items={filteredItems} onItemClick={handleSelect} />
    </div>
  );
}

10. 選択フローチャート

最後に、「どの Hook を使うべきか」を判断するためのフローチャートと早見表をまとめます。迷ったときはここに戻ってきてください!

各 Hook の判断基準まとめ

シチュエーション別にどの Hook を選ぶべきかを表にまとめました。

シチュエーション 選択する Hook
単純な値を保持したい useState
複数の関連状態を管理したい useReducer
複雑な状態遷移ロジックがある useReducer
再レンダーなしで値を保持したい useRef
DOM要素に直接アクセスしたい useRef
タイマーIDや前回値を保持したい useRef
深くネストした子に値を渡したい useContext
APIを呼び出したい useEffect
イベントリスナーを登録したい useEffect
重い計算をキャッシュしたい useMemo
オブジェクト参照を安定させたい useMemo
memo 化した子に関数を渡したい useCallback

まとめ

本記事で学んだ 7 つの Hook を一言でまとめると、以下のようになります。

Hook 一言でまとめると
useState 「値を覚えて、変わったら再描画」
useReducer 「複雑な状態変更を整理整頓」
useRef 「再描画なしで値やDOMを保持」
useContext 「Props のバケツリレーを省略」
useEffect 「レンダー後に外の世界と同期」
useMemo 「計算結果を使い回し」
useCallback 「関数を使い回し」

これらの Hook を適切に組み合わせることで、読みやすく、パフォーマンスの良い React アプリケーションを構築できます。

「useState と useEffect だけ」から卒業して、場面に応じた最適な Hook を選べるようになりましょう!

💡 React Compiler について
将来的には React Compiler が自動的にメモ化を行うため、useMemouseCallback を手動で書く必要性が減る可能性があります。ただし、Hook の役割と使い分けを理解しておくことは、今後も重要です。

0
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
0
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?