React Hooks完全ガイド2026|useState・useEffect・useContextからカスタムフックまで

React HooksはReact 16.8で導入され、関数コンポーネントでstateやライフサイクルを扱えるようにした革命的な機能です。クラスコンポーネントの複雑さを解消し、ロジックの再利用を大幅に改善しました。本記事では、主要なフックを実践的なコード例で解説します。

useState:状態管理の基礎

useStateは最も基本的なフックです。コンポーネントのローカル状態を管理します。

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState(null);
  const [form, setForm] = useState({ name: '', email: '' });

  // 関数型更新で前の状態を参照
  const increment = () => setCount(prev => prev + 1);

  // オブジェクトのスプレッド更新
  const updateName = (name) => setForm(prev => ({...prev, name}));

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
}

useEffect:副作用の管理

useEffectはデータ取得・DOM操作・サブスクリプションなどの副作用を管理します。依存配列の扱いが重要です。

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // AbortControllerでクリーンアップ
    const controller = new AbortController();

    const fetchUser = async () => {
      setLoading(true);
      try {
        const res = await fetch(`/api/users/${userId}`, {
          signal: controller.signal
        });
        const data = await res.json();
        setUser(data);
      } catch (error) {
        if (error.name !== 'AbortError') console.error(error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();

    // クリーンアップ関数
    return () => controller.abort();
  }, [userId]); // userIdが変わるたびに再実行

  if (loading) return <div>Loading...</div>;
  return <div>{user?.name}</div>;
}

useContext:グローバル状態の共有

useContextはコンテキストAPIと組み合わせて、propsのバケツリレーなしにコンポーネントツリー全体で状態を共有できます。

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

// テーマコンテキストの作成
const ThemeContext = createContext(null);

// Provider コンポーネント
export 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>
  );
}

// カスタムフックで使いやすくする
export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within ThemeProvider');
  return context;
}

// 使用例
function Header() {
  const { theme, toggleTheme } = useTheme();
  return <header className={theme}><button onClick={toggleTheme}>テーマ切替</button></header>;
}

useReducer:複雑な状態管理

useReducerは複数の値が絡み合う複雑な状態管理に適しています。Reduxに似たパターンで、状態遷移を明示的に管理できます。

import { useReducer } from 'react';

// アクションタイプ
const ActionType = {
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT',
  RESET: 'RESET',
  SET: 'SET'
};

// リデューサー関数
function counterReducer(state, action) {
  switch (action.type) {
    case ActionType.INCREMENT:
      return { ...state, count: state.count + (action.step || 1) };
    case ActionType.DECREMENT:
      return { ...state, count: state.count - (action.step || 1) };
    case ActionType.RESET:
      return { count: 0 };
    case ActionType.SET:
      return { count: action.value };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: ActionType.INCREMENT })}>+1</button>
      <button onClick={() => dispatch({ type: ActionType.DECREMENT })}>-1</button>
      <button onClick={() => dispatch({ type: ActionType.RESET })}>リセット</button>
    </div>
  );
}

useMemo・useCallback:パフォーマンス最適化

useMemoは計算コストの高い処理の結果をメモ化し、useCallbackは関数の再生成を防いでレンダリングを最適化します。

import { useMemo, useCallback, memo } from 'react';

function ProductList({ products, category, onSelect }) {
  // 重い計算のメモ化
  const filteredProducts = useMemo(
    () => products.filter(p => p.category === category),
    [products, category] // productsかcategoryが変わった時だけ再計算
  );

  // コールバック関数のメモ化
  const handleSelect = useCallback(
    (productId) => onSelect(productId),
    [onSelect]
  );

  return (
    <ul>
      {filteredProducts.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onSelect={handleSelect} // メモ化されているので子コンポーネントが不必要に再レンダリングされない
        />
      ))}
    </ul>
  );
}

// React.memoで子コンポーネントをメモ化
const ProductItem = memo(function ProductItem({ product, onSelect }) {
  return <li onClick={() => onSelect(product.id)}>{product.name}</li>;
});

カスタムフック:ロジックの再利用

カスタムフックは「use」で始まる関数で、複数のコンポーネントで共通のロジックを再利用できます。

// データ取得の汎用カスタムフック
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!isCancelled) setData(data);
      })
      .catch(err => {
        if (!isCancelled) setError(err);
      })
      .finally(() => {
        if (!isCancelled) setLoading(false);
      });

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

  return { data, loading, error };
}

// ローカルストレージ同期フック
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setStoredValue = (newValue) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  };

  return [value, setStoredValue];
}

まとめ:React Hooksで宣言的で再利用可能なコードを書こう

React HooksはReactの開発体験を根本から変えました。useState・useEffect・useContextの基本3フックをマスターしてから、useMemo・useCallback・useReducerへと学習を進めましょう。そして最終的にはカスタムフックでビジネスロジックを整理することで、メンテナブルなReactアプリケーションが完成します。

投稿者 kasata

IT企業でエンジニアとして勤務後、テクノロジー情報メディア「Tech Athletes(テック・アスリート)」を運営。プログラミング、クラウドインフラ(AWS/GCP/Azure)、AI活用、Webサービス開発を専門とする。エンジニア・ビジネスパーソン向けに、実際に使ってみた経験をもとに信頼できる技術情報を発信中。資格:AWS認定ソリューションアーキテクト、Python 3 エンジニア認定試験合格。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です