第8章: フック (Hooks)

フック (Hooks) は React 16.8 で導入された機能で、関数コンポーネントに State やライフサイクルの機能を追加します。 第5章で学んだ useState に加え、副作用を扱う useEffect、 コンテキストを利用する useContext、パフォーマンス最適化のための useCallback / useMemo、 そしてカスタムフックの作り方を解説します。

フックとは

フック (Hooks) とは、関数コンポーネントの中から React の機能にアクセスするための関数です。 名前が use で始まる関数がフックです。

React が提供する主要な組み込みフックは以下の通りです。

フック用途学習済み
useState コンポーネントに State(状態)を追加する 第5章で解説済み
useEffect 副作用(データ取得、DOM 操作、タイマーなど)を実行する この章で解説
useRef レンダーをまたいで値を保持する / DOM 要素を参照する この章で解説
useContext コンテキスト(グローバルな値の共有)を利用する この章で解説
useCallback 関数をメモ化して不要な再生成を防ぐ この章で解説
useMemo 計算結果をメモ化して不要な再計算を防ぐ この章で解説

フックのルール

フックを使う際には、必ず以下の 2 つのルールを守る必要があります。 これらのルールに違反すると、React が内部状態を正しく管理できなくなり、バグの原因になります。

ルール 1: トップレベルでのみ呼び出す
フックは if 文、ループ、ネストされた関数の中で呼び出してはいけません。 コンポーネントの関数本体のトップレベルで、常に同じ順序で呼び出す必要があります。

ルール 2: React の関数からのみ呼び出す
フックは React の関数コンポーネントか、カスタムフックの中でのみ呼び出せます。 通常の JavaScript 関数から呼び出すことはできません。

フックのルール
function MyComponent({ isVisible }) {
  // OK: トップレベルで呼び出し
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // NG: if 文の中で呼び出し
  // if (isVisible) {
  //   const [data, setData] = useState(null);
  // }

  // NG: ループの中で呼び出し
  // for (let i = 0; i < 3; i++) {
  //   useEffect(() => { ... });
  // }

  return <div>...</div>;
}

useEffect

useEffect は、コンポーネントの副作用 (Side Effect) を実行するためのフックです。 副作用とは、レンダリング以外の処理のことで、以下のようなものが含まれます。

基本的な使い方

useEffect の基本構文
import { useState, useEffect } from 'react';

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

  // レンダーのたびに実行される(依存配列なし)
  useEffect(() => {
    document.title = `クリック数: ${count}`;
  });

  return (
    <button onClick={() => setCount(count + 1)}>
      クリック: {count}
    </button>
  );
}

依存配列

useEffect の第 2 引数に依存配列 (dependency array) を渡すことで、 副作用を実行するタイミングを制御できます。

依存配列実行タイミング用途
なし(省略) 毎回のレンダー後 通常は使わない(パフォーマンスに注意)
[](空配列) 初回レンダー後の 1 回のみ 初期データの取得、イベントリスナーの登録
[value] value が変化したとき 特定の値に応じた処理
依存配列のパターン
// パターン 1: 初回のみ実行(マウント時)
useEffect(() => {
  console.log('コンポーネントがマウントされました');
}, []);  // ← 空の依存配列

// パターン 2: userId が変わるたびに実行
useEffect(() => {
  fetchUserData(userId);
}, [userId]);  // ← userId が変わったときだけ実行

// パターン 3: 複数の依存値
useEffect(() => {
  fetchSearchResults(query, page);
}, [query, page]);  // ← query または page が変わったとき

依存配列には、Effect 内で使用するすべてのリアクティブな値を含めてください。 State、Props、それらから計算された値が対象です。依存値を省略すると、 古い値を参照し続けるバグ(stale closure)が発生します。 ESLint の react-hooks/exhaustive-deps ルールが警告してくれるので活用しましょう。

実践例: データ取得

UserProfile.jsx
import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let ignore = false;  // 古いリクエストの結果を無視するフラグ

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

      try {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('取得に失敗しました');
        const data = await response.json();

        if (!ignore) {
          setUser(data);
        }
      } catch (err) {
        if (!ignore) {
          setError(err.message);
        }
      } finally {
        if (!ignore) {
          setLoading(false);
        }
      }
    }

    fetchUser();

    // クリーンアップ: userId が変わったら古いリクエストを無視
    return () => {
      ignore = true;
    };
  }, [userId]);

  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラー: {error}</p>;
  if (!user) return null;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

ignore フラグの重要性: userId が素早く変更された場合、先に発行したリクエストが後から返ってくることがあります。 ignore フラグを使うことで、古いリクエストの結果を破棄し、 常に最新の userId に対応するデータのみを State にセットできます。 これはレースコンディションを防ぐ定番パターンです。

クリーンアップ関数

useEffect のコールバック関数から関数を返すと、 それがクリーンアップ関数として実行されます。 クリーンアップは以下のタイミングで呼ばれます。

クリーンアップが必要な典型的なケースは、イベントリスナーの解除やタイマーのクリアです。

イベントリスナーのクリーンアップ
import { useState, useEffect } from 'react';

function WindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

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

    window.addEventListener('resize', handleResize);

    // クリーンアップ: リスナーを解除
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);  // 空配列: マウント時に 1 回だけ登録

  return (
    <p>ウィンドウサイズ: {size.width} x {size.height}</p>
  );
}
タイマーのクリーンアップ
import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // クリーンアップ: タイマーを停止
    return () => clearInterval(id);
  }, []);

  return <p>経過時間: {seconds}</p>;
}

クリーンアップを忘れると「メモリリーク」が発生します。 コンポーネントがアンマウントされた後もイベントリスナーやタイマーが動き続け、 存在しない State を更新しようとしてエラーが発生したり、メモリが無駄に消費されたりします。 addEventListenersetInterval を使う場合は、必ずクリーンアップを実装しましょう。

useRef

useRef は、レンダーをまたいで値を保持するためのフックです。 useState と異なり、値を変更しても再レンダーが発生しません。 主に 2 つの用途で使われます。

用途 1: DOM 要素への参照

DOM 要素への参照
import { useRef } from 'react';

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

  function handleClick() {
    // DOM 要素に直接アクセス
    inputRef.current.focus();
  }

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="入力してください" />
      <button onClick={handleClick}>フォーカス</button>
    </div>
  );
}

用途 2: 再レンダーを発生させずに値を保持

レンダー回数のカウント
import { useState, useEffect, useRef } from 'react';

function RenderCounter() {
  const [inputValue, setInputValue] = useState('');
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;  // 再レンダーは発生しない
  });

  return (
    <div>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <p>レンダー回数: {renderCount.current}</p>
    </div>
  );
}
比較項目useStateuseRef
値の変更で再レンダー する しない
レンダー間で値が保持される はい はい
DOM 要素への参照 不可 可能(ref 属性)
値のアクセス方法 変数そのもの .current プロパティ

useContext

useContext は、コンテキスト (Context) を使ってコンポーネントツリー全体に 値を共有するためのフックです。Props のバケツリレー(prop drilling)を避けることができます。

コンテキストの作成と使用

ThemeContext.jsx
import { createContext, useContext, useState } from 'react';

// 1. コンテキストを作成
const ThemeContext = createContext('light');

// 2. Provider コンポーネント(値を供給する側)
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Consumer(値を利用する側)
function ThemeButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button onClick={toggleTheme}>
      現在のテーマ: {theme}(クリックで切り替え)
    </button>
  );
}

// 4. App で Provider を配置
function App() {
  return (
    <ThemeProvider>
      <div>
        <h1>テーマ切り替えデモ</h1>
        <ThemeButton />
      </div>
    </ThemeProvider>
  );
}

Context は「テーマ」「認証情報」「言語設定」など、コンポーネントツリー全体で共有する必要がある値に適しています。 頻繁に変化する値(例: 入力中のテキスト)には向きません。Context の値が変わると、 その Context を利用するすべてのコンポーネントが再レンダーされるためです。

useCallback

useCallback は、関数をメモ化して、依存値が変わらない限り同じ関数の参照を返すフックです。 子コンポーネントに関数を渡す際、不要な再レンダーを防ぐのに役立ちます。

useCallback の使い方
import { useState, useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // count が変わらない限り、同じ関数の参照を返す
  const handleIncrement = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);  // 依存値なし(setCount は安定した参照)

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <p>カウント: {count}</p>
      {/* text が変わっても ExpensiveChild は再レンダーされない */}
      <ExpensiveChild onIncrement={handleIncrement} />
    </div>
  );
}

useCallbackReact.memo と組み合わせて使うことが多いです。 React.memo でラップされた子コンポーネントは、Props が変化しない限り再レンダーをスキップします。 useCallback で関数の参照を安定させることで、この最適化が効果的に機能します。

useMemo

useMemo は、計算結果をメモ化するフックです。 依存値が変わらない限り、前回の計算結果を再利用します。 重い計算処理をレンダーのたびに実行するのを防ぎます。

useMemo の使い方
import { useState, useMemo } from 'react';

function FilteredList({ items }) {
  const [query, setQuery] = useState('');
  const [sortOrder, setSortOrder] = useState('asc');

  // items, query, sortOrder が変わったときだけ再計算
  const filteredAndSorted = useMemo(() => {
    const filtered = items.filter(item =>
      item.name.toLowerCase().includes(query.toLowerCase())
    );

    return [...filtered].sort((a, b) =>
      sortOrder === 'asc'
        ? a.name.localeCompare(b.name)
        : b.name.localeCompare(a.name)
    );
  }, [items, query, sortOrder]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="検索..."
      />
      <ul>
        {filteredAndSorted.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

早すぎる最適化に注意: useCallbackuseMemo は強力ですが、すべての関数や計算に適用すべきではありません。 メモ化にもコスト(メモリ使用量、比較処理)がかかるため、 実際にパフォーマンスの問題が確認された場合にのみ導入しましょう。 多くの場合、React のデフォルトのレンダリングは十分に高速です。

カスタムフック

カスタムフックとは、use で始まる名前を持つ、 フックを内部で使用する通常の JavaScript 関数です。 複数のコンポーネントで共通するロジックをカスタムフックに切り出すことで、 コードの再利用性が大きく向上します。

例: useLocalStorage

hooks/useLocalStorage.js
import { useState } from 'react';

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value) => {
    const valueToStore = typeof value === 'function'
      ? value(storedValue)
      : value;
    setStoredValue(valueToStore);
    localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue];
}

export default useLocalStorage;
使用例
import useLocalStorage from './hooks/useLocalStorage';

function Settings() {
  const [name, setName] = useLocalStorage('userName', '');
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <p>テーマ: {theme}</p>
    </div>
  );
}

例: useFetch

hooks/useFetch.js
import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

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

      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const json = await response.json();
        if (!ignore) setData(json);
      } catch (err) {
        if (!ignore) setError(err.message);
      } finally {
        if (!ignore) setLoading(false);
      }
    }

    fetchData();
    return () => { ignore = true; };
  }, [url]);

  return { data, loading, error };
}

export default useFetch;
使用例
import useFetch from './hooks/useFetch';

function UserList() {
  const { data: users, loading, error } = useFetch('/api/users');

  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラー: {error}</p>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

カスタムフックの命名規則は use で始めることです。 これにより ESLint のフックルールが正しく適用されます。 カスタムフック自体は State を「共有」するわけではなく、 State を管理するロジックを共有します。 各コンポーネントは独立した State を持ちます。

フック一覧まとめ

フック用途典型的な使い方
useState State の管理 フォーム入力、トグル状態、カウンター
useEffect 副作用の実行 API 通信、タイマー、DOM 操作
useRef 値の保持 / DOM 参照 input へのフォーカス、前回の値の保持
useContext グローバルな値の共有 テーマ、認証情報、言語設定
useCallback 関数のメモ化 子コンポーネントへの関数渡し
useMemo 計算結果のメモ化 重いフィルタリング・ソート処理
カスタムフック ロジックの再利用 useFetch、useLocalStorage など