React useフックまとめ|基本の5つから始める状態管理

React useフックの基本的な使い方をまとめて解説。useState、useEffect、useContext、useReducer、useCallbackの実践的な活用方法を初心者向けに説明します。

Learning Next 運営
69 分で読めます

みなさん、React Hooksを使って開発していますか?

「どのフックをどの場面で使えばいいの?」 「似たようなフックがあって、使い分けが分からない」 「状態管理が複雑になってきて、整理したい」

こんな悩みを持っている方も多いでしょう。

React Hooksは関数コンポーネントで状態管理や副作用を扱うための強力な機能です。 しかし、種類が多くて最初は戸惑ってしまいますよね。

この記事では、React Hooksの基本から実践的な活用方法まで、初心者向けに詳しく解説します。 まずは基本の5つのフックをマスターして、効率的な状態管理を身につけていきましょう!

React Hooksって何?基本を理解しよう

React Hooksは、関数コンポーネントで状態や副作用を管理するためのAPIです。

従来のクラスコンポーネントでしかできなかったことが、関数コンポーネントでもできるようになりました。

クラスコンポーネントとの違いを見てみよう

まずは、従来の書き方と比較してみましょう。

// クラスコンポーネント(従来の方法)
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  
  componentDidMount() {
    console.log('コンポーネントがマウントされました');
  }
  
  componentDidUpdate() {
    console.log('コンポーネントが更新されました');
  }
  
  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };
  
  render() {
    return (
      <div>
        <p>カウント: {this.state.count}</p>
        <button onClick={this.handleIncrement}>+1</button>
      </div>
    );
  }
}

クラスコンポーネントでは、stateやライフサイクルメソッドを使っていました。 コードが長くて、書くのも読むのも大変ですよね。

では、React Hooksを使うとどうなるでしょうか?

// 関数コンポーネント + Hooks(現在推奨の方法)
import { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log('コンポーネントがマウントまたは更新されました');
  });
  
  const handleIncrement = () => {
    setCount(count + 1);
  };
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={handleIncrement}>+1</button>
    </div>
  );
};

Hooksを使うことで、同じ機能をより短く簡潔に書けるようになりました。

Hooksを使うメリット

React Hooksには、たくさんのメリットがあります。

1. コードの再利用が簡単

// ロジックを再利用できるカスタムフック
const useCounter = (initialValue = 0) => {
  const [count, setCount] = useState(initialValue);
  
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialValue);
  
  return { count, increment, decrement, reset };
};

// 複数のコンポーネントで同じロジックを再利用
const CounterA = () => {
  const { count, increment, decrement } = useCounter(0);
  
  return (
    <div>
      <p>カウンターA: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};

const CounterB = () => {
  const { count, increment, reset } = useCounter(10);
  
  return (
    <div>
      <p>カウンターB: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={reset}>リセット</button>
    </div>
  );
};

カスタムフックを作ることで、同じロジックを複数のコンポーネントで使い回せます。

2. 関心事の分離ができる

const UserProfile = ({ userId }) => {
  // ユーザー情報の管理
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  // フォーム状態の管理
  const [isEditing, setIsEditing] = useState(false);
  const [formData, setFormData] = useState({});
  
  // ユーザー情報の取得
  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
        setFormData(userData);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, [userId]);
  
  // 編集モードの切り替え
  const toggleEdit = () => {
    setIsEditing(!isEditing);
    if (isEditing) {
      setFormData(user); // 編集をキャンセル
    }
  };
  
  return (
    <div>
      {loading ? (
        <p>読み込み中...</p>
      ) : (
        <div>
          <h2>{user.name}</h2>
          <button onClick={toggleEdit}>
            {isEditing ? 'キャンセル' : '編集'}
          </button>
        </div>
      )}
    </div>
  );
};

それぞれのフックが特定の関心事を担当するため、コードが理解しやすくなります。

Hooksの基本ルール

React Hooksを使う時には、重要なルールがあります。

ルール1: フックの呼び出し順序を守る

// ✅ 正しい使用方法
const MyComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  useEffect(() => {
    // 副作用処理
  }, []);
  
  return <div>{count} - {name}</div>;
};

// ❌ 間違った使用方法
const MyComponent = ({ shouldShow }) => {
  const [count, setCount] = useState(0);
  
  // 条件付きでフックを呼び出すのはNG
  if (shouldShow) {
    const [name, setName] = useState(''); // これはエラーになる
  }
  
  return <div>{count}</div>;
};

フックは常に同じ順序で呼び出される必要があります。 条件分岐やループの中でフックを呼び出してはいけません。

ルール2: 関数コンポーネント内でのみ使用

// ✅ 正しい使用方法
const MyComponent = () => {
  const [state, setState] = useState(0);
  return <div>{state}</div>;
};

// ✅ カスタムフック内での使用
const useCustomHook = () => {
  const [state, setState] = useState(0);
  return [state, setState];
};

// ❌ 間違った使用方法
const regularFunction = () => {
  const [state, setState] = useState(0); // これはエラーになる
  return state;
};

フックは、React の関数コンポーネントまたはカスタムフック内でのみ使用できます。

useState - 状態管理の基本

useStateは、コンポーネントの状態を管理するためのフックです。

一番よく使うフックなので、しっかりと理解しておきましょう。

基本的な使い方

import { useState } from 'react';

const SimpleCounter = () => {
  // [現在の値, 更新関数] = useState(初期値)
  const [count, setCount] = useState(0);
  
  const handleIncrement = () => {
    setCount(count + 1);
  };
  
  const handleDecrement = () => {
    setCount(count - 1);
  };
  
  return (
    <div>
      <h2>カウンター</h2>
      <p>現在の値: {count}</p>
      <button onClick={handleIncrement}>+1</button>
      <button onClick={handleDecrement}>-1</button>
    </div>
  );
};

useStateは配列を返します。 1つ目が現在の値、2つ目が値を更新するための関数です。

様々なデータ型の状態管理

文字列、数値、真偽値、配列、オブジェクトなど、様々なデータ型を管理できます。

const FormComponent = () => {
  // 文字列の状態
  const [name, setName] = useState('');
  
  // 数値の状態
  const [age, setAge] = useState(0);
  
  // 真偽値の状態
  const [isSubscribed, setIsSubscribed] = useState(false);
  
  // 配列の状態
  const [items, setItems] = useState([]);
  
  // オブジェクトの状態
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });
  
  const handleAddItem = () => {
    setItems([...items, `アイテム ${items.length + 1}`]);
  };
  
  const handleUserUpdate = (field, value) => {
    setUser(prevUser => ({
      ...prevUser,
      [field]: value
    }));
  };
  
  return (
    <div>
      <h2>フォーム</h2>
      
      <div>
        <input
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="名前"
        />
      </div>
      
      <div>
        <input
          type="number"
          value={age}
          onChange={(e) => setAge(parseInt(e.target.value))}
          placeholder="年齢"
        />
      </div>
      
      <div>
        <label>
          <input
            type="checkbox"
            checked={isSubscribed}
            onChange={(e) => setIsSubscribed(e.target.checked)}
          />
          メルマガを購読する
        </label>
      </div>
      
      <div>
        <button onClick={handleAddItem}>アイテム追加</button>
        <ul>
          {items.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
      </div>
    </div>
  );
};

それぞれのデータ型に適した状態管理ができます。

状態更新のパターンを覚えよう

状態の更新には、いくつかのパターンがあります。

直接的な更新

const DirectUpdate = () => {
  const [count, setCount] = useState(0);
  
  const handleUpdate = () => {
    setCount(10); // 直接値を設定
  };
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={handleUpdate}>10に設定</button>
    </div>
  );
};

新しい値を直接設定する方法です。 シンプルで分かりやすいですね。

関数による更新

const FunctionalUpdate = () => {
  const [count, setCount] = useState(0);
  
  const handleIncrement = () => {
    // 前の値を受け取って新しい値を返す
    setCount(prevCount => prevCount + 1);
  };
  
  const handleDoubleIncrement = () => {
    // 複数回の更新を正しく処理
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
  };
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={handleIncrement}>+1</button>
      <button onClick={handleDoubleIncrement}>+2</button>
    </div>
  );
};

前の値に基づいて更新する場合は、関数を使用します。 複数回の更新を正しく処理できるため、安全な方法です。

オブジェクトの更新

const ObjectUpdate = () => {
  const [user, setUser] = useState({
    name: '田中太郎',
    age: 25,
    email: 'tanaka@example.com'
  });
  
  const updateName = (newName) => {
    setUser(prevUser => ({
      ...prevUser,
      name: newName
    }));
  };
  
  const updateAge = (newAge) => {
    setUser(prevUser => ({
      ...prevUser,
      age: newAge
    }));
  };
  
  const resetUser = () => {
    setUser({
      name: '',
      age: 0,
      email: ''
    });
  };
  
  return (
    <div>
      <h2>ユーザー情報</h2>
      <p>名前: {user.name}</p>
      <p>年齢: {user.age}</p>
      <p>メール: {user.email}</p>
      
      <button onClick={() => updateName('佐藤花子')}>
        名前を変更
      </button>
      <button onClick={() => updateAge(30)}>
        年齢を変更
      </button>
      <button onClick={resetUser}>
        リセット
      </button>
    </div>
  );
};

オブジェクトの更新では、スプレッド演算子を使って immutable な更新を行います。

これにより、Reactが変更を正しく検知できるようになります。

TODOリストで実践してみよう

実践的な例として、TODOリストを作ってみましょう。

const TodoList = () => {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');
  
  const addTodo = () => {
    if (inputValue.trim()) {
      setTodos(prevTodos => [
        ...prevTodos,
        {
          id: Date.now(),
          text: inputValue,
          completed: false
        }
      ]);
      setInputValue('');
    }
  };
  
  const toggleTodo = (id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  };
  
  const deleteTodo = (id) => {
    setTodos(prevTodos =>
      prevTodos.filter(todo => todo.id !== id)
    );
  };
  
  return (
    <div>
      <h2>TODOリスト</h2>
      
      <div>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="TODOを入力"
        />
        <button onClick={addTodo}>追加</button>
      </div>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <label>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => toggleTodo(todo.id)}
              />
              <span style={{
                textDecoration: todo.completed ? 'line-through' : 'none'
              }}>
                {todo.text}
              </span>
            </label>
            <button onClick={() => deleteTodo(todo.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

このコードでは、配列の追加、更新、削除を適切に処理しています。

どの操作も、元の配列を変更せずに新しい配列を作成している点がポイントです。

useEffect - 副作用を上手に管理しよう

useEffectは、副作用(API呼び出し、タイマー、DOMの操作など)を管理するためのフックです。

従来のライフサイクルメソッドの役割を担っています。

基本的な使い方

import { useState, useEffect } from 'react';

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    console.log('コンポーネントがマウントされました');
    
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        console.error('ユーザー取得エラー:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, []); // 空の依存配列 = マウント時のみ実行
  
  if (loading) return <div>読み込み中...</div>;
  if (!user) return <div>ユーザーが見つかりません</div>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
};

空の依存配列を指定することで、マウント時のみ実行されます。

依存配列による制御

useEffectの実行タイミングは、依存配列で制御できます。

const SearchResults = () => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
    
    const searchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
        const data = await response.json();
        setResults(data);
      } catch (error) {
        console.error('検索エラー:', error);
      } finally {
        setLoading(false);
      }
    };
    
    searchData();
  }, [query]); // queryが変更されるたびに実行
  
  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="検索キーワード"
      />
      
      {loading && <p>検索中...</p>}
      
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
};

依存配列にqueryを指定することで、検索キーワードが変更されるたびに検索が実行されます。

クリーンアップ関数でメモリリークを防ごう

副作用の処理が終わったら、適切にクリーンアップすることが重要です。

タイマーのクリーンアップ

const Timer = () => {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  
  useEffect(() => {
    let intervalId;
    
    if (isRunning) {
      intervalId = setInterval(() => {
        setSeconds(prevSeconds => prevSeconds + 1);
      }, 1000);
    }
    
    // クリーンアップ関数
    return () => {
      if (intervalId) {
        clearInterval(intervalId);
      }
    };
  }, [isRunning]);
  
  const handleStart = () => setIsRunning(true);
  const handleStop = () => setIsRunning(false);
  const handleReset = () => {
    setIsRunning(false);
    setSeconds(0);
  };
  
  return (
    <div>
      <h2>タイマー</h2>
      <p>{seconds}秒</p>
      <button onClick={handleStart}>開始</button>
      <button onClick={handleStop}>停止</button>
      <button onClick={handleReset}>リセット</button>
    </div>
  );
};

クリーンアップ関数を返すことで、タイマーを適切に停止できます。 これにより、メモリリークを防げます。

イベントリスナーのクリーンアップ

const WindowSize = () => {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    
    // クリーンアップ関数
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
  
  return (
    <div>
      <p>ウィンドウサイズ: {windowSize.width} x {windowSize.height}</p>
    </div>
  );
};

イベントリスナーも、適切にクリーンアップすることが重要です。

複数のuseEffectで関心事を分離しよう

1つのコンポーネントで複数の副作用を扱う場合は、複数のuseEffectに分離しましょう。

const UserDashboard = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [theme, setTheme] = useState('light');
  
  // ユーザー情報の取得
  useEffect(() => {
    const fetchUser = async () => {
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      setUser(userData);
    };
    
    fetchUser();
  }, [userId]);
  
  // 投稿一覧の取得
  useEffect(() => {
    const fetchPosts = async () => {
      const response = await fetch(`/api/users/${userId}/posts`);
      const postsData = await response.json();
      setPosts(postsData);
    };
    
    fetchPosts();
  }, [userId]);
  
  // テーマの保存
  useEffect(() => {
    localStorage.setItem('theme', theme);
    document.body.className = theme;
  }, [theme]);
  
  return (
    <div>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        {theme === 'light' ? 'ダークモード' : 'ライトモード'}
      </button>
      
      {user && (
        <div>
          <h2>{user.name}</h2>
          <p>投稿数: {posts.length}</p>
        </div>
      )}
    </div>
  );
};

それぞれの関心事に対して独立したuseEffectを使用しています。

これにより、コードが理解しやすくなり、保守性も向上します。

useContext - アプリ全体で状態を共有しよう

useContextは、コンテキストの値を取得するためのフックです。

プロップスの受け渡しが深くなる「プロップドリリング」を解決できます。

基本的な使い方

テーマ(ライト/ダークモード)を管理するコンテキストを作ってみましょう。

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

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

// コンテキストプロバイダー
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

// カスタムフックの作成
const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};

まずは、コンテキストとプロバイダーを作成します。

// コンポーネントでの使用
const Header = () => {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <header style={{
      backgroundColor: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#333' : '#fff',
      padding: '1rem'
    }}>
      <h1>アプリケーション</h1>
      <button onClick={toggleTheme}>
        {theme === 'light' ? 'ダークモード' : 'ライトモード'}
      </button>
    </header>
  );
};

const Content = () => {
  const { theme } = useTheme();
  
  return (
    <main style={{
      backgroundColor: theme === 'light' ? '#f5f5f5' : '#222',
      color: theme === 'light' ? '#333' : '#fff',
      padding: '2rem',
      minHeight: '400px'
    }}>
      <p>現在のテーマ: {theme}</p>
    </main>
  );
};

// アプリケーションのルート
const App = () => {
  return (
    <ThemeProvider>
      <Header />
      <Content />
    </ThemeProvider>
  );
};

useThemeを使うことで、どのコンポーネントからでもテーマの状態にアクセスできます。

実践的な認証コンテキスト

より実践的な例として、認証機能を管理するコンテキストを作ってみましょう。

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

const AuthContext = createContext();

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // 初期認証チェック
    const checkAuth = async () => {
      try {
        const token = localStorage.getItem('authToken');
        if (token) {
          const response = await fetch('/api/me', {
            headers: { Authorization: `Bearer ${token}` }
          });
          if (response.ok) {
            const userData = await response.json();
            setUser(userData);
          }
        }
      } catch (error) {
        console.error('認証チェックエラー:', error);
      } finally {
        setLoading(false);
      }
    };
    
    checkAuth();
  }, []);
  
  const login = async (email, password) => {
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });
      
      if (response.ok) {
        const { user, token } = await response.json();
        localStorage.setItem('authToken', token);
        setUser(user);
        return { success: true };
      } else {
        return { success: false, error: 'ログインに失敗しました' };
      }
    } catch (error) {
      return { success: false, error: 'ネットワークエラー' };
    }
  };
  
  const logout = () => {
    localStorage.removeItem('authToken');
    setUser(null);
  };
  
  return (
    <AuthContext.Provider value={{ user, login, logout, loading }}>
      {children}
    </AuthContext.Provider>
  );
};

認証状態をアプリケーション全体で管理できるプロバイダーです。

// 使用例
const LoginForm = () => {
  const { login } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    const result = await login(email, password);
    if (result.success) {
      console.log('ログイン成功');
    } else {
      console.error(result.error);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="メールアドレス"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="パスワード"
      />
      <button type="submit">ログイン</button>
    </form>
  );
};

const UserProfile = () => {
  const { user, logout } = useAuth();
  
  if (!user) {
    return <LoginForm />;
  }
  
  return (
    <div>
      <h2>ようこそ、{user.name}さん</h2>
      <button onClick={logout}>ログアウト</button>
    </div>
  );
};

どのコンポーネントからでも、認証関連の機能にアクセスできます。

複数のコンテキストを組み合わせよう

複数のコンテキストを組み合わせて使用することも可能です。

// 複数のプロバイダーの組み合わせ
const AppProviders = ({ children }) => {
  return (
    <AuthProvider>
      <ThemeProvider>
        <SettingsProvider>
          {children}
        </SettingsProvider>
      </ThemeProvider>
    </AuthProvider>
  );
};

// 複数のコンテキストを使用するコンポーネント
const Dashboard = () => {
  const { user } = useAuth();
  const { theme } = useTheme();
  const { settings } = useSettings();
  
  return (
    <div style={{
      backgroundColor: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#333' : '#fff'
    }}>
      <h1>ダッシュボード</h1>
      <p>ユーザー: {user?.name}</p>
      <p>言語: {settings.language}</p>
      <p>通知: {settings.notifications ? 'ON' : 'OFF'}</p>
    </div>
  );
};

複数のコンテキストを組み合わせることで、様々な状態を管理できます。

useReducer - 複雑な状態管理をスマートに

useReducerは、複雑な状態更新ロジックを管理するためのフックです。

useStateよりも高度な状態管理が可能になります。

基本的な使い方

まずは、シンプルなカウンターの例から見てみましょう。

import { useReducer } from 'react';

// アクションの定義
const initialState = { count: 0 };

const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return initialState;
    case 'SET':
      return { count: action.payload };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
};

const Counter = () => {
  const [state, dispatch] = useReducer(counterReducer, initialState);
  
  return (
    <div>
      <p>カウント: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>
        +1
      </button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>
        -1
      </button>
      <button onClick={() => dispatch({ type: 'RESET' })}>
        リセット
      </button>
      <button onClick={() => dispatch({ type: 'SET', payload: 10 })}>
        10に設定
      </button>
    </div>
  );
};

reducer関数により、状態更新のロジックをコンポーネントから分離できます。

これにより、複雑な状態管理が整理されます。

複雑なフォーム状態の管理

実践的なフォーム状態管理の例を見てみましょう。

const initialFormState = {
  values: {
    name: '',
    email: '',
    message: ''
  },
  errors: {},
  touched: {},
  isSubmitting: false
};

const formReducer = (state, action) => {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        values: {
          ...state.values,
          [action.field]: action.value
        }
      };
    
    case 'SET_TOUCHED':
      return {
        ...state,
        touched: {
          ...state.touched,
          [action.field]: true
        }
      };
    
    case 'SET_ERROR':
      return {
        ...state,
        errors: {
          ...state.errors,
          [action.field]: action.error
        }
      };
    
    case 'CLEAR_ERROR':
      const newErrors = { ...state.errors };
      delete newErrors[action.field];
      return {
        ...state,
        errors: newErrors
      };
    
    case 'SET_SUBMITTING':
      return {
        ...state,
        isSubmitting: action.isSubmitting
      };
    
    case 'RESET_FORM':
      return initialFormState;
    
    default:
      return state;
  }
};

フォームの複雑な状態(値、エラー、タッチ状態、送信状態)を一元管理できます。

const ContactForm = () => {
  const [formState, dispatch] = useReducer(formReducer, initialFormState);
  
  const validate = (field, value) => {
    let error = '';
    
    switch (field) {
      case 'name':
        if (!value.trim()) error = '名前は必須です';
        break;
      case 'email':
        if (!value.trim()) {
          error = 'メールアドレスは必須です';
        } else if (!/\S+@\S+\.\S+/.test(value)) {
          error = 'メールアドレスの形式が正しくありません';
        }
        break;
      case 'message':
        if (!value.trim()) error = 'メッセージは必須です';
        break;
    }
    
    return error;
  };
  
  const handleChange = (field, value) => {
    dispatch({ type: 'SET_FIELD', field, value });
    
    // バリデーション
    const error = validate(field, value);
    if (error) {
      dispatch({ type: 'SET_ERROR', field, error });
    } else {
      dispatch({ type: 'CLEAR_ERROR', field });
    }
  };
  
  const handleBlur = (field) => {
    dispatch({ type: 'SET_TOUCHED', field });
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // すべてのフィールドをバリデーション
    const fields = ['name', 'email', 'message'];
    let hasErrors = false;
    
    fields.forEach(field => {
      const error = validate(field, formState.values[field]);
      if (error) {
        dispatch({ type: 'SET_ERROR', field, error });
        hasErrors = true;
      }
    });
    
    if (hasErrors) return;
    
    dispatch({ type: 'SET_SUBMITTING', isSubmitting: true });
    
    try {
      await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formState.values)
      });
      
      dispatch({ type: 'RESET_FORM' });
      alert('送信完了');
    } catch (error) {
      console.error('送信エラー:', error);
    } finally {
      dispatch({ type: 'SET_SUBMITTING', isSubmitting: false });
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>名前</label>
        <input
          type="text"
          value={formState.values.name}
          onChange={(e) => handleChange('name', e.target.value)}
          onBlur={() => handleBlur('name')}
        />
        {formState.errors.name && formState.touched.name && (
          <span style={{ color: 'red' }}>{formState.errors.name}</span>
        )}
      </div>
      
      <div>
        <label>メールアドレス</label>
        <input
          type="email"
          value={formState.values.email}
          onChange={(e) => handleChange('email', e.target.value)}
          onBlur={() => handleBlur('email')}
        />
        {formState.errors.email && formState.touched.email && (
          <span style={{ color: 'red' }}>{formState.errors.email}</span>
        )}
      </div>
      
      <div>
        <label>メッセージ</label>
        <textarea
          value={formState.values.message}
          onChange={(e) => handleChange('message', e.target.value)}
          onBlur={() => handleBlur('message')}
        />
        {formState.errors.message && formState.touched.message && (
          <span style={{ color: 'red' }}>{formState.errors.message}</span>
        )}
      </div>
      
      <button type="submit" disabled={formState.isSubmitting}>
        {formState.isSubmitting ? '送信中...' : '送信'}
      </button>
    </form>
  );
};

複雑なフォーム状態を、useReducerで効率的に管理できます。

TODOアプリケーションの実装

より実践的な例として、高機能なTODOアプリケーションを作ってみましょう。

const initialTodoState = {
  todos: [],
  filter: 'all', // 'all', 'active', 'completed'
  nextId: 1
};

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: state.nextId,
            text: action.text,
            completed: false,
            createdAt: new Date().toISOString()
          }
        ],
        nextId: state.nextId + 1
      };
    
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.id
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
    
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.id)
      };
    
    case 'EDIT_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.id
            ? { ...todo, text: action.text }
            : todo
        )
      };
    
    case 'SET_FILTER':
      return {
        ...state,
        filter: action.filter
      };
    
    case 'CLEAR_COMPLETED':
      return {
        ...state,
        todos: state.todos.filter(todo => !todo.completed)
      };
    
    default:
      return state;
  }
};

const TodoApp = () => {
  const [state, dispatch] = useReducer(todoReducer, initialTodoState);
  const [inputValue, setInputValue] = useState('');
  
  const handleAddTodo = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      dispatch({ type: 'ADD_TODO', text: inputValue });
      setInputValue('');
    }
  };
  
  const filteredTodos = state.todos.filter(todo => {
    switch (state.filter) {
      case 'active':
        return !todo.completed;
      case 'completed':
        return todo.completed;
      default:
        return true;
    }
  });
  
  return (
    <div>
      <h1>TODOアプリ</h1>
      
      <form onSubmit={handleAddTodo}>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="TODOを入力"
        />
        <button type="submit">追加</button>
      </form>
      
      <div>
        <button
          onClick={() => dispatch({ type: 'SET_FILTER', filter: 'all' })}
          style={{ fontWeight: state.filter === 'all' ? 'bold' : 'normal' }}
        >
          すべて
        </button>
        <button
          onClick={() => dispatch({ type: 'SET_FILTER', filter: 'active' })}
          style={{ fontWeight: state.filter === 'active' ? 'bold' : 'normal' }}
        >
          未完了
        </button>
        <button
          onClick={() => dispatch({ type: 'SET_FILTER', filter: 'completed' })}
          style={{ fontWeight: state.filter === 'completed' ? 'bold' : 'normal' }}
        >
          完了済み
        </button>
      </div>
      
      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
            />
            <span style={{
              textDecoration: todo.completed ? 'line-through' : 'none'
            }}>
              {todo.text}
            </span>
            <button
              onClick={() => dispatch({ type: 'DELETE_TODO', id: todo.id })}
            >
              削除
            </button>
          </li>
        ))}
      </ul>
      
      <div>
        <p>総数: {state.todos.length}</p>
        <p>完了: {state.todos.filter(t => t.completed).length}</p>
        <p>未完了: {state.todos.filter(t => !t.completed).length}</p>
        <button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>
          完了済みを削除
        </button>
      </div>
    </div>
  );
};

useReducerにより、複雑なTODOアプリケーションの状態管理が整理されます。

どの操作も、明確なアクションとして定義されているため、理解しやすく保守性も高いコードになります。

useCallback - パフォーマンスを最適化しよう

useCallbackは、関数のメモ化を行うためのフックです。

不要な関数の再作成を防ぎ、パフォーマンスを向上させることができます。

基本的な使い方

import { useState, useCallback } from 'react';

const ExpensiveChild = ({ onClick, name }) => {
  console.log(`${name} がレンダリングされました`);
  
  return (
    <button onClick={onClick}>
      {name}
    </button>
  );
};

const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // useCallbackを使用しない場合
  const handleClickWithoutCallback = () => {
    console.log('クリックされました');
  };
  
  // useCallbackを使用する場合
  const handleClickWithCallback = useCallback(() => {
    console.log('クリックされました');
  }, []); // 依存配列が空なので、一度だけ作成される
  
  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="名前を入力"
      />
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      
      <ExpensiveChild 
        onClick={handleClickWithoutCallback} 
        name="再作成される関数"
      />
      <ExpensiveChild 
        onClick={handleClickWithCallback} 
        name="メモ化された関数"
      />
    </div>
  );
};

useCallbackを使うことで、不要な関数の再作成を防げます

子コンポーネントの不要な再レンダリングも防止できます。

依存配列を活用しよう

useCallbackでも、useEffectと同様に依存配列を使って制御できます。

const SearchComponent = () => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  const performSearch = useCallback(async (searchQuery) => {
    if (!searchQuery.trim()) {
      setResults([]);
      return;
    }
    
    setLoading(true);
    try {
      const response = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`);
      const data = await response.json();
      setResults(data);
    } catch (error) {
      console.error('検索エラー:', error);
    } finally {
      setLoading(false);
    }
  }, []); // 依存配列が空なので、一度だけ作成される
  
  const handleSearch = useCallback(() => {
    performSearch(query);
  }, [query, performSearch]); // queryが変更されるたびに再作成
  
  const handleClear = useCallback(() => {
    setQuery('');
    setResults([]);
  }, []); // 依存配列が空なので、一度だけ作成される
  
  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="検索キーワード"
      />
      <button onClick={handleSearch}>検索</button>
      <button onClick={handleClear}>クリア</button>
      
      {loading && <p>検索中...</p>}
      
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
};

依存配列により、適切なタイミングで関数を再作成できます。

React.memoと組み合わせてパフォーマンス向上

子コンポーネントの最適化には、React.memoと組み合わせるのが効果的です。

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

// メモ化された子コンポーネント
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
  console.log(`TodoItem ${todo.id} がレンダリングされました`);
  
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span style={{
        textDecoration: todo.completed ? 'line-through' : 'none'
      }}>
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>削除</button>
    </li>
  );
});

const TodoList = () => {
  const [todos, setTodos] = useState([
    { id: 1, text: 'React学習', completed: false },
    { id: 2, text: 'TypeScript学習', completed: true }
  ]);
  const [inputValue, setInputValue] = useState('');
  
  const handleToggle = useCallback((id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);
  
  const handleDelete = useCallback((id) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  }, []);
  
  const handleAdd = useCallback(() => {
    if (inputValue.trim()) {
      setTodos(prevTodos => [
        ...prevTodos,
        {
          id: Date.now(),
          text: inputValue,
          completed: false
        }
      ]);
      setInputValue('');
    }
  }, [inputValue]);
  
  return (
    <div>
      <div>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="TODOを入力"
        />
        <button onClick={handleAdd}>追加</button>
      </div>
      
      <ul>
        {todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={handleToggle}
            onDelete={handleDelete}
          />
        ))}
      </ul>
    </div>
  );
};

React.memoとuseCallbackを組み合わせることで、効率的な最適化が可能です。

変更されていないTodoItemは再レンダリングされないため、パフォーマンスが向上します。

カスタムフックでの活用

カスタムフック内でもuseCallbackは有効です。

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

const useApi = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [url]);
  
  const refresh = useCallback(() => {
    fetchData();
  }, [fetchData]);
  
  useEffect(() => {
    fetchData();
  }, [fetchData]);
  
  return { data, loading, error, refresh };
};

// 使用例
const UserProfile = ({ userId }) => {
  const { data: user, loading, error, refresh } = useApi(`/api/users/${userId}`);
  
  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラー: {error}</p>;
  if (!user) return <p>ユーザーが見つかりません</p>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={refresh}>更新</button>
    </div>
  );
};

カスタムフック内でuseCallbackを使用することで、再利用可能で高性能なフックを作成できます。

まとめ

React Hooksは、関数コンポーネントで状態管理と副作用を扱うための強力なツールです。

基本の5つのフックをおさらいしてみましょう。

  • useState: シンプルな状態管理に最適
  • useEffect: 副作用の管理とクリーンアップ
  • useContext: アプリ全体での状態共有
  • useReducer: 複雑な状態更新ロジックの管理
  • useCallback: 関数のメモ化とパフォーマンス最適化

実践で活用するポイント

  • 適切な選択: 単純な状態はuseState、複雑な状態はuseReducer
  • 副作用の制御: useEffectの依存配列を正しく設定
  • グローバル状態: useContextでプロップドリリングを解決
  • パフォーマンス: useCallbackで不要な再レンダリングを防止

開発で注意すべきこと

  • フックのルール: 呼び出し順序と使用場所を守る
  • 依存配列: 適切な依存関係を指定する
  • 関心の分離: 複数のuseEffectで責任を分離
  • カスタムフック: ロジックの再利用と抽象化

これから学ぶべきこと

  • 他のフック: useMemo、useRef、useImperativeHandleなど
  • カスタムフック: 独自のフックを作成してロジックを再利用
  • 状態管理ライブラリ: Redux、Zustand、Jotaiなどとの使い分け
  • TypeScript: 型安全なフックの使い方

React Hooksを適切に活用することで、保守性が高く、パフォーマンスに優れたReactアプリケーションを構築できます。

従来のクラスコンポーネントよりも簡潔で理解しやすいコードが書けるようになります。

最初は慣れないかもしれませんが、基本の5つのフックをマスターすることで、React開発がもっと楽しくなるはずです。

ぜひ、実際のプロジェクトでReact Hooksを活用して、その便利さを体験してみてください!

関連記事