Web Development

React Hooks完全ガイド - useState, useEffect, useMemo, useCallback徹底解説

kitahara-dev2025/11/0114 min read
React Hooks完全ガイド - useState, useEffect, useMemo, useCallback徹底解説

React Hooks完全ガイド

React Hooksは、React 16.8で導入され、関数コンポーネントで状態管理やライフサイクル機能を使用できるようにする強力な機能です。このガイドでは、全てのHooksを実践的な例とともに徹底解説します。

📋 目次

  1. useState - 状態管理の基本
  2. useEffect - 副作用の処理
  3. useContext - グローバル状態管理
  4. useReducer - 複雑な状態管理
  5. useMemo - 計算結果のメモ化
  6. useCallback - 関数のメモ化
  7. useRef - 値の永続化とDOM操作
  8. useImperativeHandle - 子コンポーネントの公開API
  9. useLayoutEffect - 同期的な副作用
  10. カスタムHooks - ロジックの再利用

1. useState - 状態管理の基本

最も基本的なHook。コンポーネントに状態を追加します。

基本的な使い方

import { useState } from 'react';

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

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

関数的更新(重要!)

// ❌ 悪い例:古い値を参照する可能性
setCount(count + 1);

// ✅ 良い例:常に最新の値を使用
setCount(prevCount => prevCount + 1);

遅延初期化

// 初期値の計算が重い場合
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation();
  return initialState;
});

複数の状態管理

// ❌ 悪い例:関連する状態を別々に管理
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [age, setAge] = useState(0);

// ✅ 良い例:関連する状態をオブジェクトでグループ化
const [user, setUser] = useState({
  firstName: '',
  lastName: '',
  age: 0,
});

// 更新時は不変性を保つ
setUser(prevUser => ({
  ...prevUser,
  firstName: 'John',
}));

2. useEffect - 副作用の処理

データフェッチ、サブスクリプション、DOM操作などの副作用を処理します。

基本的な使い方

import { useEffect } from 'react';

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 副作用:データフェッチ
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);

    // クリーンアップ関数(オプション)
    return () => {
      // コンポーネントのアンマウント時やeffect再実行前に実行
      console.log('Cleanup');
    };
  }, [userId]); // 依存配列

  return user ? <div>{user.name}</div> : <div>Loading...</div>;
}

依存配列の重要性

// パターン1: マウント時のみ実行
useEffect(() => {
  console.log('Component mounted');
}, []); // 空配列

// パターン2: 毎レンダリング後に実行(非推奨)
useEffect(() => {
  console.log('After every render');
}); // 依存配列なし

// パターン3: 特定の値が変更されたときのみ実行
useEffect(() => {
  console.log('userId changed:', userId);
}, [userId]); // userIdが変更されたときのみ

クリーンアップの重要性

// WebSocketの例
useEffect(() => {
  const ws = new WebSocket('wss://example.com');

  ws.onmessage = (event) => {
    setMessages(prev => [...prev, event.data]);
  };

  // クリーンアップでWebSocketを閉じる
  return () => {
    ws.close();
  };
}, []);

// イベントリスナーの例
useEffect(() => {
  const handleResize = () => {
    setWindowWidth(window.innerWidth);
  };

  window.addEventListener('resize', handleResize);

  // クリーンアップでイベントリスナーを削除
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

よくある間違い

// ❌ 間違い:useEffectの中でstateを直接更新して無限ループ
useEffect(() => {
  setCount(count + 1); // 無限ループ!
});

// ✅ 正しい:依存配列を適切に指定
useEffect(() => {
  if (shouldUpdate) {
    setCount(prevCount => prevCount + 1);
  }
}, [shouldUpdate]);

3. useContext - グローバル状態管理

Prop Drillingを避けて、コンポーネントツリー全体で値を共有します。

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

// Contextの作成
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// Providerコンポーネント
export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

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

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

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

// 使用例
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button
      onClick={toggleTheme}
      className={`bg-${theme === 'light' ? 'white' : 'gray-900'}`}
    >
      Toggle Theme
    </button>
  );
}

4. useReducer - 複雑な状態管理

複数の関連する状態や複雑な更新ロジックがある場合に使用します。

import { useReducer } from 'react';

// 状態の型定義
interface State {
  count: number;
  step: number;
}

// アクションの型定義
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  | { type: 'setStep'; payload: number };

// Reducer関数
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 'reset':
      return { count: 0, step: 1 };
    case 'setStep':
      return { ...state, step: action.payload };
    default:
      return state;
  }
}

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

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <input
        type="number"
        value={state.step}
        onChange={(e) => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
      />
    </div>
  );
}

5. useMemo - 計算結果のメモ化

重い計算結果をメモ化してパフォーマンスを最適化します。

import { useMemo } from 'react';

function ExpensiveList({ items }: { items: Item[] }) {
  // 重い計算をメモ化
  const sortedItems = useMemo(() => {
    console.log('Sorting items...');
    return items
      .filter(item => item.active)
      .sort((a, b) => b.priority - a.priority);
  }, [items]); // itemsが変更されたときのみ再計算

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

useMemoを使うべき場合

// ✅ 使うべき:重い計算
const expensiveValue = useMemo(() => {
  return items.reduce((acc, item) => {
    return acc + complexCalculation(item);
  }, 0);
}, [items]);

// ❌ 不要:軽い計算
const doubledValue = useMemo(() => count * 2, [count]); // オーバーヘッドの方が大きい

// ✅ 使うべき:参照の安定性が必要な場合
const config = useMemo(() => ({
  api: '/api/data',
  timeout: 3000,
}), []); // 毎回新しいオブジェクトを作らない

6. useCallback - 関数のメモ化

関数をメモ化して、子コンポーネントの不要な再レンダリングを防ぎます。

import { useCallback, memo } from 'react';

// 子コンポーネント(memo化)
const ExpensiveChild = memo(({ onClick }: { onClick: () => void }) => {
  console.log('Child rendered');
  return <button onClick={onClick}>Click</button>;
});

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

  // ❌ 悪い例:毎回新しい関数が作られる
  const handleClick = () => {
    setCount(c => c + 1);
  };

  // ✅ 良い例:関数をメモ化
  const handleClickMemo = useCallback(() => {
    setCount(c => c + 1);
  }, []); // 依存配列が空なので関数は常に同じ

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <ExpensiveChild onClick={handleClickMemo} />
    </div>
  );
}

useCallbackとuseMemoの使い分け

// useCallback: 関数をメモ化
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b]
);

// useMemo: 値をメモ化
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

// useCallbackは実質的にuseMemoの糖衣構文
useCallback(fn, deps) === useMemo(() => fn, deps)

7. useRef - 値の永続化とDOM操作

レンダリング間で値を保持したり、DOM要素を直接操作したりします。

import { useRef, useEffect } from 'react';

function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // マウント時にinputにフォーカス
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} type="text" />;
}

// レンダリングをトリガーせずに値を保持
function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef<number>();

  const start = () => {
    if (!intervalRef.current) {
      intervalRef.current = window.setInterval(() => {
        setCount(c => c + 1);
      }, 1000);
    }
  };

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

  useEffect(() => {
    return () => stop(); // クリーンアップ
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

8. useImperativeHandle - 子コンポーネントの公開API

親コンポーネントが子コンポーネントのインスタンスメソッドを呼び出せるようにします。

import { forwardRef, useImperativeHandle, useRef } from 'react';

interface FancyInputHandle {
  focus: () => void;
  reset: () => void;
}

const FancyInput = forwardRef<FancyInputHandle>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current?.focus();
    },
    reset: () => {
      if (inputRef.current) {
        inputRef.current.value = '';
      }
    },
  }));

  return <input ref={inputRef} type="text" />;
});

// 使用例
function Parent() {
  const inputRef = useRef<FancyInputHandle>(null);

  return (
    <div>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>Focus</button>
      <button onClick={() => inputRef.current?.reset()}>Reset</button>
    </div>
  );
}

9. useLayoutEffect - 同期的な副作用

DOMの変更後、ブラウザの描画前に同期的に実行されます。

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

function Tooltip() {
  const [tooltipHeight, setTooltipHeight] = useState(0);
  const tooltipRef = useRef<HTMLDivElement>(null);

  // useEffectではちらつきが発生する可能性
  // useLayoutEffectは描画前に実行されるのでちらつきなし
  useLayoutEffect(() => {
    const height = tooltipRef.current?.getBoundingClientRect().height ?? 0;
    setTooltipHeight(height);
  }, []);

  return (
    <div
      ref={tooltipRef}
      style={{
        position: 'absolute',
        top: `${-tooltipHeight}px`, // 高さに基づいて配置
      }}
    >
      Tooltip content
    </div>
  );
}

10. カスタムHooks - ロジックの再利用

実践的なカスタムHooksの例。

useLocalStorage

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

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue] as const;
}

// 使用例
function App() {
  const [name, setName] = useLocalStorage('name', '');
  return <input value={name} onChange={(e) => setName(e.target.value)} />;
}

useDebounce

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// 使用例:検索フィールド
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearchTerm) {
      // 500ms後にAPI呼び出し
      searchAPI(debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);

  return (
    <input
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="Search..."
    />
  );
}

useAsync

interface AsyncState<T> {
  loading: boolean;
  data: T | null;
  error: Error | null;
}

function useAsync<T>(asyncFunction: () => Promise<T>, immediate = true) {
  const [state, setState] = useState<AsyncState<T>>({
    loading: immediate,
    data: null,
    error: null,
  });

  const execute = useCallback(async () => {
    setState({ loading: true, data: null, error: null });
    try {
      const data = await asyncFunction();
      setState({ loading: false, data, error: null });
      return data;
    } catch (error) {
      setState({ loading: false, data: null, error: error as Error });
      throw error;
    }
  }, [asyncFunction]);

  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [execute, immediate]);

  return { ...state, execute };
}

// 使用例
function UserProfile({ userId }: { userId: string }) {
  const { loading, data, error } = useAsync(
    () => fetch(`/api/users/${userId}`).then(res => res.json()),
    true
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!data) return null;

  return <div>{data.name}</div>;
}

📝 まとめ

React Hooksの重要ポイント:

  • useState - 状態管理の基本、関数的更新を活用
  • useEffect - 副作用処理、依存配列とクリーンアップが重要
  • useContext - Prop Drilling回避、グローバル状態管理
  • useReducer - 複雑な状態ロジックに最適
  • useMemo - 重い計算をメモ化してパフォーマンス向上
  • useCallback - 関数をメモ化して再レンダリング防止
  • useRef - レンダリングをトリガーせず値を保持
  • カスタムHooks - ロジックを再利用可能に

Hooksを理解することで、React開発の生産性とコード品質が大幅に向上します!

🔗 参考リンク

#React#JavaScript#Hooks#TypeScript

著者について

kitahara-devによって執筆されました。