react-hooks
プログラミング

React Hooksとは?useStateからカスタムフックまで完全マスター

目次

現代のReact開発を語る上で、React Hooks(フック)の理解は絶対に欠かせません。2019年にReact 16.8で正式に導入されて以来、フックはReactコンポーネントの書き方を根本から変え、よりシンプルで直感的な開発を可能にしました。本記事では、「フックって一体何?」という基本から、主要なフックの使い方、ロジックを再利用するためのカスタムフック、そしてパフォーマンスを意識したベストプラクティスまで、React Hooksの全体像を網羅的に解説します。

React Hooksが切り拓いた新しい開発パラダイム

クラスコンポーネントからの解放:なぜフックが生まれたのか

React Hooksの登場は、単なる新機能の追加ではなく、React開発の根本的なパラダイムシフトでした。フックが解決しようとした問題を理解することで、その真の価値が見えてきます。

フック登場以前のReact開発では、stateやライフサイクルメソッドを使用するためには、必ずクラスコンポーネントを書く必要がありました。典型的なクラスコンポーネントは以下のような形でした:

javascript

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    // thisバインディングが必要
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Count: {this.state.count}
      </button>
    );
  }
}

このコードには、いくつかの本質的な課題がありました。

thisバインディングの複雑さは、多くの開発者を悩ませました。JavaScriptのthisは、呼び出し方によって参照先が変わる特殊な挙動を持ちます。上記の例では、constructorで明示的にbindするか、アロー関数を使う必要があり、これは初心者にとって大きな障壁となっていました。

ロジックの分散問題も深刻でした。例えば、データの取得とクリーンアップという関連する処理が、異なるライフサイクルメソッドに分かれてしまいます:

javascript

class UserProfile extends React.Component {
  componentDidMount() {
    // データ取得の開始
    this.fetchUserData(this.props.userId);
    // イベントリスナーの登録
    window.addEventListener('resize', this.handleResize);
  }

  componentWillUnmount() {
    // クリーンアップ処理が別の場所に
    window.removeEventListener('resize', this.handleResize);
  }

  // 関連する処理が分散してしまう
}

React Hooksは、これらすべての問題を優雅に解決しました。同じカウンターをフックで書くと:

javascript

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

コードは劇的にシンプルになり、thisの問題も完全に解消されました。

関数コンポーネントの完全な勝利

フックの導入により、関数コンポーネントはクラスコンポーネントのすべての機能を獲得しました。それどころか、多くの面でクラスコンポーネントを上回る利点を持つようになりました。

コードの簡潔性は最も顕著な利点です。同じ機能を実装するのに必要なコード量を比較してみましょう:

javascript

// クラスコンポーネント(約20行)
class Timer extends React.Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 };
  }

  componentDidMount() {
    this.interval = setInterval(() => {
      this.setState({ seconds: this.state.seconds + 1 });
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    return <div>Seconds: {this.state.seconds}</div>;
  }
}

// 関数コンポーネント with Hooks(約10行)
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return <div>Seconds: {seconds}</div>;
}

関数コンポーネントの方が約半分のコード量で、より読みやすく実装できています。

フックのルール:なぜ制約があるのか

React Hooksには、使用する際に守らなければならない2つの重要なルールがあります。

ルール1:トップレベルでのみ呼び出す

javascript

// ❌ 悪い例:条件文の中でフックを呼び出している
function BadComponent({ shouldTrack }) {
  if (shouldTrack) {
    useEffect(() => {
      console.log('Tracking...');
    }, []);
  }
}

// ✅ 良い例:フックは常にトップレベルで呼び出す
function GoodComponent({ shouldTrack }) {
  useEffect(() => {
    if (shouldTrack) {
      console.log('Tracking...');
    }
  }, [shouldTrack]);
}

このルールが存在する理由は、Reactがフックの呼び出し順序に依存してstateを管理しているからです。条件によってフックの呼び出しがスキップされると、Reactは各フックがどのstateに対応するかを正しく追跡できなくなります。

基本フック3つ:React開発の土台

useState:状態管理の基礎

useStateは、最も基本的で最も頻繁に使用されるフックです。その使い方を詳しく見ていきましょう。

基本的な使い方:

javascript

function LoginForm() {
  // 文字列の状態
  const [username, setUsername] = useState('');
  // 真偽値の状態
  const [isLoading, setIsLoading] = useState(false);
  // オブジェクトの状態
  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    
    try {
      await loginAPI({ username, ...formData });
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        disabled={isLoading}
      />
      {/* 他のフィールド */}
    </form>
  );
}

関数型更新の重要性:

javascript

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

  const handleMultipleIncrements = () => {
    // ❌ 悪い例:古い値を参照してしまう可能性
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    // 結果:1しか増えない

    // ✅ 良い例:関数型更新で常に最新の値を参照
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
    // 結果:3増える
  };
}

遅延初期化によるパフォーマンス最適化:

javascript

function ExpensiveComponent() {
  // ❌ 悪い例:毎回のレンダリングで高コストな計算が実行される
  const [data, setData] = useState(calculateExpensiveInitialData());

  // ✅ 良い例:初回レンダリング時のみ実行される
  const [data, setData] = useState(() => calculateExpensiveInitialData());
}

useEffect:副作用の管理

useEffectは、Reactコンポーネントにおける副作用を管理するための中心的なフックです。

基本的なデータフェッチング:

javascript

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

  useEffect(() => {
    // 非同期処理を扱う方法
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // userIdが変更されたときに再実行

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>Welcome, {user?.name}!</div>;
}

クリーンアップ関数の重要性:

javascript

function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    // イベントリスナーの登録
    window.addEventListener('mousemove', handleMouseMove);

    // クリーンアップ関数で後片付け
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // 空の依存配列 = マウント時のみ実行

  return (
    <div>
      Mouse position: {position.x}, {position.y}
    </div>
  );
}

useRef:参照の保持とDOM操作

useRefは、再レンダリングをトリガーせずに値を保持したり、DOM要素への参照を保持したりするためのフックです。

DOM要素への参照:

javascript

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

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

  return (
    <input
      ref={inputRef}
      placeholder="I will be focused automatically"
    />
  );
}

値の保持(前回の値を記憶):

javascript

function ValueChangeDetector({ value }) {
  const previousValue = useRef();

  useEffect(() => {
    if (previousValue.current !== undefined && previousValue.current !== value) {
      console.log(`Value changed from ${previousValue.current} to ${value}`);
    }
    previousValue.current = value;
  });

  return <div>Current value: {value}</div>;
}

高度な標準フック:より洗練された状態管理

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

useContextを使用すると、深いコンポーネントツリーでもprops drillingなしにデータを共有できます。

javascript

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

// プロバイダーコンポーネント
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>
  );
}

// 深くネストしたコンポーネントでの使用
function DeepChildComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button 
      onClick={toggleTheme}
      style={{ 
        background: theme === 'dark' ? '#333' : '#fff',
        color: theme === 'dark' ? '#fff' : '#333'
      }}
    >
      Current theme: {theme}
    </button>
  );
}

useReducer:複雑な状態遷移の管理

useReducerは、複雑な状態ロジックを管理するのに適しています。

javascript

// フォームの状態管理の例
const initialState = {
  username: '',
  email: '',
  password: '',
  errors: {},
  isSubmitting: false
};

function formReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return {
        ...state,
        [action.field]: action.value,
        errors: {
          ...state.errors,
          [action.field]: '' // フィールド更新時にエラーをクリア
        }
      };
    
    case 'SET_ERRORS':
      return { ...state, errors: action.errors };
    
    case 'SUBMIT_START':
      return { ...state, isSubmitting: true };
    
    case 'SUBMIT_SUCCESS':
      return { ...initialState }; // 成功時はリセット
    
    case 'SUBMIT_ERROR':
      return { ...state, isSubmitting: false };
    
    default:
      return state;
  }
}

function SignUpForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleChange = (e) => {
    dispatch({
      type: 'UPDATE_FIELD',
      field: e.target.name,
      value: e.target.value
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    dispatch({ type: 'SUBMIT_START' });
    
    try {
      await submitForm(state);
      dispatch({ type: 'SUBMIT_SUCCESS' });
    } catch (error) {
      dispatch({ type: 'SUBMIT_ERROR' });
      dispatch({ type: 'SET_ERRORS', errors: error.validationErrors });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={state.username}
        onChange={handleChange}
        disabled={state.isSubmitting}
      />
      {state.errors.username && (
        <span className="error">{state.errors.username}</span>
      )}
      {/* 他のフィールド */}
    </form>
  );
}

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

これらのフックは、不要な再計算や再生成を防ぐために使用します。

javascript

function ExpensiveList({ items, filter }) {
  // 高コストなフィルタリング処理をメモ化
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]); // これらが変更されたときのみ再計算

  // 子コンポーネントに渡す関数をメモ化
  const handleItemClick = useCallback((id) => {
    console.log(`Item ${id} clicked`);
  }, []); // 依存配列が空 = 関数は一度だけ作成される

  return (
    <div>
      {filteredItems.map(item => (
        <MemoizedItem
          key={item.id}
          item={item}
          onClick={handleItemClick}
        />
      ))}
    </div>
  );
}

// React.memoでラップされた子コンポーネント
const MemoizedItem = React.memo(({ item, onClick }) => {
  console.log(`Rendering item ${item.id}`);
  return (
    <div onClick={() => onClick(item.id)}>
      {item.name}
    </div>
  );
});

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

実用的なカスタムフックの例

カスタムフックは、複雑なロジックを再利用可能な形で抽出する強力な手段です。

useLocalStorage – ローカルストレージと同期するstate:

javascript

function useLocalStorage(key, initialValue) {
  // 初期値の取得
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error loading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // 値の更新
  const setValue = useCallback((value) => {
    try {
      // 関数型更新もサポート
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(`Error saving localStorage key "${key}":`, error);
    }
  }, [key, storedValue]);

  return [storedValue, setValue];
}

// 使用例
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);

  return (
    <div>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Toggle theme: {theme}
      </button>
      <input
        type="range"
        value={fontSize}
        onChange={(e) => setFontSize(Number(e.target.value))}
        min="12"
        max="24"
      />
    </div>
  );
}

useFetch – データフェッチングの抽象化:

javascript

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

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

    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(url, options);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        
        if (!cancelled) {
          setData(data);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    };

    fetchData();

    // クリーンアップで実行中のリクエストをキャンセル
    return () => {
      cancelled = true;
    };
  }, [url]); // URLが変更されたら再フェッチ

  return { data, loading, error };
}

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

  if (loading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error}</div>;

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

useDebounce – 値の変更を遅延させる:

javascript

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

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

  return debouncedValue;
}

// 使用例:検索フィールド
function SearchUsers() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  
  const { data: results } = useFetch(
    debouncedSearchTerm ? `/api/users/search?q=${debouncedSearchTerm}` : null
  );

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search users..."
      />
      
      {results && (
        <ul>
          {results.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

ベストプラクティスとよくある落とし穴

依存配列の正しい理解と管理

依存配列は、React Hooksを使いこなす上で最も重要で、最も誤解されやすい概念です。

javascript

function SearchResults({ query, filters }) {
  const [results, setResults] = useState([]);

  // ❌ 悪い例:必要な依存を含めていない
  useEffect(() => {
    searchAPI(query, filters).then(setResults);
  }, [query]); // filtersの変更が検知されない!

  // ✅ 良い例:すべての依存を含める
  useEffect(() => {
    searchAPI(query, filters).then(setResults);
  }, [query, filters]);

  // ⚠️ 注意:オブジェクトの比較
  // filtersがオブジェクトの場合、毎回新しい参照になる可能性がある
  // その場合は、個別のプロパティを依存配列に含めるか、useMemoを使用
  useEffect(() => {
    searchAPI(query, filters).then(setResults);
  }, [query, filters.category, filters.priceRange]); // より安定
}

パフォーマンスの罠と最適化戦略

React Hooksを使用する際のパフォーマンスに関する注意点:

javascript

function ProductList({ products }) {
  // ❌ 悪い例:すべてをメモ化
  const memoizedProducts = useMemo(() => products, [products]);
  const handleClick = useCallback((id) => {
    console.log(id);
  }, []);

  // ✅ 良い例:本当に必要な場合のみメモ化
  // 高コストな計算の場合
  const expensiveCalculation = useMemo(() => {
    return products.reduce((acc, product) => {
      // 複雑な計算...
      return acc + complexCalculation(product);
    }, 0);
  }, [products]);

  // 子コンポーネントが React.memo でラップされている場合
  const handleAddToCart = useCallback((productId) => {
    addToCart(productId);
  }, [addToCart]);

  return (
    <div>
      {products.map(product => (
        <MemoizedProductCard
          key={product.id}
          product={product}
          onAddToCart={handleAddToCart}
        />
      ))}
    </div>
  );
}

デバッグテクニック

React Hooksのデバッグに役立つテクニック:

javascript

// カスタムフックのデバッグ
function useCustomHook(value) {
  const [state, setState] = useState(value);

  // React DevTools で表示される
  useDebugValue(state > 100 ? 'High' : 'Low');

  // より詳細な情報を表示
  useDebugValue(state, (state) => {
    return `State: ${state}, Category: ${state > 100 ? 'High' : 'Low'}`;
  });

  return [state, setState];
}

// 依存配列の変更を追跡
function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changedProps = {};

      allKeys.forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changedProps[key] = {
            from: previousProps.current[key],
            to: props[key]
          };
        }
      });

      if (Object.keys(changedProps).length) {
        console.log('[why-did-you-update]', name, changedProps);
      }
    }

    previousProps.current = props;
  });
}

まとめ:React Hooksがもたらした革命

React Hooksは、単なる新機能ではなく、React開発の根本的な変革でした。クラスコンポーネントの複雑さから解放され、より直感的で再利用可能なコードを書けるようになりました。

基本的なuseStateとuseEffectから始まり、高度なuseReducerやuseMemo、そしてカスタムフックの作成まで、段階的に学んでいくことで、React Hooksの真の力を引き出せます。

重要なのは、フックは道具であり、適切に使用してこそ価値を発揮するということです。すべての問題をフックで解決しようとするのではなく、適材適所で使い分けることが大切です。

この記事で紹介したコード例とベストプラクティスを参考に、ぜひ実際に手を動かしてReact Hooksの世界を探求してみてください。最初は戸惑うかもしれませんが、一度その力を理解すれば、もうクラスコンポーネントには戻れなくなるでしょう。

ビジネスの成長をサポートします

Harmonic Societyは、最新のテクノロジーとクリエイティブな発想で、
お客様のビジネス課題を解決します。

豊富な実績と経験
最新技術への対応
親身なサポート体制

師田 賢人

Harmonic Society株式会社 代表取締役。一橋大学(商学部)卒業後、Accenture Japanに入社。ITコンサルタントとして働いた後、Webエンジニアを経て2016年に独立。ブロックチェーン技術を専門に200名以上の専門家に取材をし記事を執筆する。2023年にHarmonic Society株式会社を設立後、AI駆動開発によるWebサイト・アプリ制作を行っている。

コメントを残す