Reactの基礎 - カスタムHook

提供: MochiuWiki : SUSE, EC, PCB

概要

カスタムHookは、コンポーネント間でステートフルなロジックを再利用するための仕組みである。
useStateuseEffect 等の組み込みHookを組み合わせることにより、複雑な処理をカプセル化して、複数のコンポーネントから呼び出せる独自の関数として定義できる。

カスタムHookの命名規則として、関数名は必ず use で始める必要がある。
(例: useOnlineStatususeLocalStorage)

この規則は、ESLintのHooksプラグインがカスタムHookを検出するための判定基準でもあり、省略するとHooksのルール違反を検出できなくなるため厳守する必要がある。

カスタムHookの重要な特性として、同じHookを複数のコンポーネントで使用しても、各コンポーネントが保持するステートは独立している。
コンポーネントAとコンポーネントBが同じ useCounter を呼び出しても、それぞれのカウンター値は独立して管理される。

カスタムHookを活用することで、以下に示すメリットが得られる。

カスタムHookのメリット
項目 説明
ロジックの再利用 同じステートフルなロジックを複数コンポーネントで共有できる。
関心の分離 UIとロジックを分離して、コンポーネントをシンプルに保てる。
テスト容易性 ロジック単体を renderHook でテストできる。
可読性の向上 複雑な処理に意味のある名前を付けてカプセル化できる。



カスタムHookの基本

命名規則

ReactのカスタムHook関数の名前は、必ず use プレフィックスで始めるキャメルケース (camelCase) で記述する。

下表に、命名の正誤例を示す。

カスタムHook命名の正誤例
命名例 判定 説明
useOnlineStatus 正しい use プレフィックスがある。
getOnlineStatus 誤り use プレフィックスがなく、ESLintが検出できない。
useLocalStorage 正しい use プレフィックスがある。
localStorageHook 誤り use プレフィックスがなく、Hooksのルール違反を検出できない。
useFetch 正しい use プレフィックスがある。


ESLintの eslint-plugin-react-hooks プラグインは、use プレフィックスを持つ関数内でのみHooksのルール (条件付き呼び出し禁止等) を検証する。
use プレフィックスを省略すると、この検証が機能しなくなり、バグの検出が困難になる。

基本的な構造

カスタムHookは、useStateuseEffect を組み合わせてロジックをカプセル化して、必要な値や関数を返す。

オンライン状態を監視するカスタムHookの例を以下に示す。

 import { useState, useEffect } from 'react';
 
 /**
  * ユーザのオンライン状態を監視するカスタムHook
  * Webブラウザのオンライン / オフラインイベントを監視して、ネットワーク接続状態を返す
  * 
  * @returns 現在のオンライン状態 (true: オンライン, false: オフライン)
  */
 function useOnlineStatus(): boolean {
    // オンライン状態を管理
    // 初期値はnavigator.onLineで現在の接続状態を取得
    const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine);
 
    // オンライン / オフラインイベントのリスナーを登録
    useEffect(() => {
       const handleOnline = () => setIsOnline(true);    // オンラインになった時に呼ばれるハンドラ
       const handleOffline = () => setIsOnline(false);  // オフラインになった時に呼ばれるハンドラ
 
       // イベントリスナーを登録
       // onlineイベント : ネットワーク接続が回復した時にイベント発生
       window.addEventListener('online', handleOnline);
       
       // offlineイベント : ネットワーク接続が切断された時にイベント発生
       window.addEventListener('offline', handleOffline);
 
       // クリーンアップ関数 : コンポーネントのアンマウント時に実行
       // イベントリスナーを解除してメモリリークを防止
       return () => {
          window.removeEventListener('online', handleOnline);
          window.removeEventListener('offline', handleOffline);
       };
    }, []); // 空の依存配列: 初回レンダリング時のみ実行
 
    // 現在のオンライン状態を返す
    return isOnline;
 }
 
 // 使用例 : ステータスバーでの接続状態表示
 function StatusBar() {
    // カスタムフックでオンライン状態を取得
    const isOnline = useOnlineStatus();
 
    // 接続状態に応じてメッセージを表示
    return <p>{isOnline ? 'オンライン' : 'オフライン'}</p>;
 }



TypeScriptでの型定義

引数と戻り値の型

TypeScriptでカスタムHookを定義する場合は、引数と戻り値の型を明示することで、使用側での型安全性が向上する。

  • カウンタHookの型定義の例
     import { useState } from 'react';
     
     // 引数と戻り値の型を明示する
     function useCounter(initialValue: number): [number, () => void, () => void] {
        const [count, setCount] = useState<number>(initialValue);
     
        const increment = () => setCount((prev) => prev + 1);
        const decrement = () => setCount((prev) => prev - 1);
     
        return [count, increment, decrement];
     }
     
     // 使用例
     function Counter() {
        const [count, increment, decrement] = useCounter(0);
        return (
           <div>
              <button onClick={decrement}>-</button>
              <span>{count}</span>
              <button onClick={increment}>+</button>
           </div>
        );
     }
    

  • ジェネリック型を使用することで、型を柔軟に指定できる再利用可能なHookを定義できる。
     import { useState } from 'react';
     
     // ジェネリック型を使った汎用的なHook
     function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
        const [storedValue, setStoredValue] = useState<T>(() => {
           try {
              const item = window.localStorage.getItem(key);
              return item ? (JSON.parse(item) as T) : initialValue;
           }
           catch {
              return initialValue;
           }
        });
     
        const setValue = (value: T) => {
           try {
              setStoredValue(value);
              window.localStorage.setItem(key, JSON.stringify(value));
           } catch (error) {
              console.error(error);
           }
        };
     
        return [storedValue, setValue];
     }
    


戻り値の設計

カスタムHookの戻り値には、タプル形式とオブジェクト形式の2種類があり、戻り値の数と用途によって使い分ける。

下表に、それぞれの特徴と使い分けの指針を示す。

タプル形式 と オブジェクト形式の比較
項目 タプル形式 オブジェクト形式
戻り値の数 2〜3個程度が適切 4個以上に対応しやすい。
命名の自由度 分割代入時に任意の名前を付けられる。 プロパティ名が固定される。
IDE補完 型の順序に依存する。 プロパティ名で補完が効きやすい。
主な用途 useStateuseReducer と同様の形式 戻り値の意味を明確にする場合


オブジェクト形式の戻り値の例を以下に示す。

 import { useState } from 'react';
 
 interface UseFormInput {
    value: string;
    setValue: (v: string) => void;
    reset: () => void;
    isEmpty: boolean;
 }
 
 function useFormInput(initialValue: string): UseFormInput {
    const [value, setValue] = useState<string>(initialValue);
    const reset = () => setValue(initialValue);
    const isEmpty = value.trim() === '';
 
    return { value, setValue, reset, isEmpty };
 }
 
 // 使用例
 function LoginForm() {
    const username = useFormInput('');
    const password = useFormInput('');
 
    return (
       <form>
          <input value={username.value} onChange={(e) => username.setValue(e.target.value)} />
          <input value={password.value} onChange={(e) => password.setValue(e.target.value)} />
          <button onClick={username.reset}>リセット</button>
       </form>
    );
 }



サンプルコード : カスタムHook

useLocalStorage

useLocalStorage は、ローカルストレージとReactのステートを自動的に同期するHookである。

以下の例では、他タブからの変更を検知するために storage イベントを購読している。

 import { useState, useEffect } from 'react';
 
 function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
    const [storedValue, setStoredValue] = useState<T>(() => {
       if (typeof window === 'undefined') return initialValue;
       try {
          const item = window.localStorage.getItem(key);
          return item ? (JSON.parse(item) as T) : initialValue;
       }
       catch {
          return initialValue;
       }
    });
 
    const setValue = (value: T) => {
       try {
          setStoredValue(value);
          window.localStorage.setItem(key, JSON.stringify(value));
       }
       catch (error) {
          console.error(error);
       }
    };
 
    // 他タブでの変更を検知する
    useEffect(() => {
       const handleStorageChange = (e: StorageEvent) => {
          if (e.key === key && e.newValue !== null) {
             setStoredValue(JSON.parse(e.newValue) as T);
          }
       };
       window.addEventListener('storage', handleStorageChange);
       return () => window.removeEventListener('storage', handleStorageChange);
    }, [key]);
 
    return [storedValue, setValue];
 }


useFetch

useFetch は、データフェッチングの状態 (dataloadingerror) を管理するHookである。

以下の例では、ignore フラグを使用して、コンポーネントのアンマウント後のステート更新を防止している。

 import { useState, useEffect } from 'react';
 
 // データフェッチング結果の型定義
 // T: 取得するデータの型
 interface UseFetchResult<T> {
    data: T | null;       // 取得したデータ (成功時)
    loading: boolean;     // ローディング中フラグ
    error: Error | null;  // エラーオブジェクト (失敗時)
 }
 
 /**
  * 指定したURLからデータを非同期で取得するカスタムフック
  * データフェッチングの状態 (data, loading, error) を統一的に管理する
  * 
  * @template T - 取得するデータの型
  * @param url  - データを取得するエンドポイントのURL
  * @returns データ、ローディング状態、エラーを含むオブジェクト
  */
 function useFetch<T>(url: string): UseFetchResult<T> {
    const [data, setData] = useState<T | null>(null);        // 取得したデータを管理する状態
    const [loading, setLoading] = useState<boolean>(true);   // ローディング状態を管理 (初期値はtrue:即座にフェッチ開始するため)
    const [error, setError] = useState<Error | null>(null);  // エラー情報を管理
 
    useEffect(() => {
       // アンマウント後のステート更新を防止するためのフラグ
       // クリーンアップ関数でtrueに設定される
       let ignore = false;
 
       // データを非同期で取得する内部関数
       const fetchData = async () => {
          setLoading(true);  // ローディング開始
          setError(null);    // エラーをクリア
          try {
             const response = await fetch(url);          // 指定されたURLからデータをフェッチ
 
             // HTTPエラーレスポンスの場合は例外をスロー
             if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
 
             const json = (await response.json()) as T;  // レスポンスボディをJSONとしてパース
 
             // アンマウントされていない場合のみデータをセット
             if (!ignore) setData(json);
          }
          catch (err) {
             // エラーハンドリング : アンマウントされていない場合のみエラーをセット
             if (!ignore) setError(err instanceof Error ? err : new Error(String(err)));
          }
          finally {
             // 成功 / 失敗に関わらずローディング状態を解除
             if (!ignore) setLoading(false);
          }
       };
 
       // データ取得を実行
       fetchData();
 
       // クリーンアップ関数 : アンマウント時にignoreフラグをtrueに設定
       // これにより、アンマウント後のステート更新が防止される (メモリリーク対策)
       return () => {
          ignore = true;
       };
    }, [url]); // URLが変更されたら再フェッチ
 
    // 結果をオブジェクトとして返す
    return { data, loading, error };
 }


useDebounce

useDebounce は、値の変化を指定した遅延時間だけ遅らせるHookである。
これは、検索ボックスへの入力等、頻繁に変化する値に対してAPIコールを抑制する用途に使用する。

 import { useState, useEffect } from 'react';
 
 /**
  * 値の変化を指定した遅延時間だけ遅らせるカスタムHook
  * 検索ボックスへの入力等、頻繁に変化する値に対してAPIコール等を抑制する用途に使用する
  * 
  * @template T  - 対象となる値の型
  * @param value - デバウンス対象の値
  * @param delay - 遅延時間 (ミリ秒)
  * @returns 遅延後の値
  */
 function useDebounce<T>(value: T, delay: number): T {
    // デバウンス後の値を管理する状態
    const [debouncedValue, setDebouncedValue] = useState<T>(value);
 
    useEffect(() => {
       // 指定した遅延時間後に値を更新するタイマを設定
       const timer = setTimeout(() => {
          setDebouncedValue(value);
       }, delay);
 
       // クリーンアップ関数 : 値が変わるたびにタイマをリセット
       // これにより、連続して値が変わった場合は最後の変更のみが反映される
       return () => clearTimeout(timer);
    }, [value, delay]);  // valueまたはdelayが変更されたら再実行
 
    // デバウンス後の値を返す
    return debouncedValue;
 }
 
 // 使用例 : 検索ボックスでのAPI呼び出し最適化
 function SearchBox() {
    const [inputValue, setInputValue] = useState('');     // 入力値を管理する状態
    const debouncedValue = useDebounce(inputValue, 500);  // 入力値を500ms遅延させる (入力が止まってから500[ms]後に値が確定)
 
    // デバウンス後の値が変わったらAPIコールを実行
    useEffect(() => {
       if (debouncedValue) {
          // 500ms入力が止まった後にAPIコールを実行する
          console.log('検索:', debouncedValue);
       }
    }, [debouncedValue]);
 
    // 検索入力欄を描画
    return <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />;
 }


useMediaQuery

useMediaQuery は、CSSメディアクエリの一致状態を監視するHookである。
これは、レスポンシブなUI制御をJavaScript側で行う場合に使用する。

 import { useState, useEffect } from 'react';
 
 /**
  * CSSメディアクエリの一致状態を監視するカスタムHook
  * レスポンシブなUI制御をJavaScript側で行う場合に使用する
  * 
  * @param query - メディアクエリ文字列 (例: '(max-width: 768px)')
  * @returns メディアクエリに一致しているかどうかのブール値
  */
 function useMediaQuery(query: string): boolean {
    // メディアクエリの一致状態を管理
    // 初期値は関数形式で設定(SSR対応:windowが存在しない場合はfalse)
    const [matches, setMatches] = useState<boolean>(() => {
       // サーバサイドレンダリング時はwindowが存在しないためfalseを返す
       if (typeof window === 'undefined') return false;
       
       // 初期描画時にメディアクエリの現在の一致状態を取得
       return window.matchMedia(query).matches;
    });
 
    useEffect(() => {
       // SSR時は処理をスキップ
       if (typeof window === 'undefined') return;
 
       // MediaQueryListオブジェクトを取得
       const mediaQueryList = window.matchMedia(query);
 
       // メディアクエリの一致状態が変化した時に呼ばれるハンドラ
       const handleChange = (e: MediaQueryListEvent) => setMatches(e.matches);
 
       // メディアクエリの変化をリッスン開始
       mediaQueryList.addEventListener('change', handleChange);
 
       // クリーンアップ関数 : リスナーを削除
       return () => mediaQueryList.removeEventListener('change', handleChange);
    }, [query]); // クエリが変更されたら再設定
 
    // 現在の一致状態を返す
    return matches;
 }
 
 // 使用例 : レスポンシブレイアウトの切り替え
 function ResponsiveLayout() {
    // 画面幅が768[px]以下かどうかを判定
    const isMobile = useMediaQuery('(max-width: 768px)');
 
    // 画面サイズに応じて表示を切り替え
    return <div>{isMobile ? 'モバイルレイアウト' : 'デスクトップレイアウト'}</div>;
 }


useToggle

useToggle は、真偽値の切り替え状態を管理する簡単なHookである。
これは、モーダルの開閉やアコーディオンの展開等、オン / オフの切り替えが必要な場面で使用する。

 import { useState, useCallback } from 'react';
 
 /**
  * 真偽値の切り替え状態を管理するカスタムHook
  * モーダルの開閉やアコーディオンの展開等、オン / オフの切り替えが必要な場面で使用する
  * 
  * @param initialValue - 初期値 (デフォルト: false)
  * @returns [現在の値, 切り替え関数] のタプル
  */
 function useToggle(initialValue: boolean = false): [boolean, () => void] {
    // 現在の真偽値を管理する状態
    const [value, setValue] = useState<boolean>(initialValue);
 
    // 値を反転させる関数
    // useCallbackでメモ化して、再レンダリング間で同一参照を維持
    const toggle = useCallback(() => setValue((prev) => !prev), []);
 
    // 現在の値と切り替え関数をタプルとして返す
    // useStateと同じ形式で、分割代入して使用可能
    return [value, toggle];
 }
 
 // 使用例 : モーダルの開閉制御
 function Modal() {
    // モーダルの開閉状態を管理
    // isOpen      : 現在の状態 (初期値はfalse = 閉じている)
    // toggleModal : 状態を反転させる関数
    const [isOpen, toggleModal] = useToggle(false);
 
    return (
       <div>
          {/* ボタンクリックでモーダルの開閉を切り替え */}
          <button onClick={toggleModal}>
             {isOpen ? 'モーダルを閉じる' : 'モーダルを開く'}
          </button>
          {/* isOpenがtrueの場合のみモーダルを表示 */}
          {isOpen && <div className="modal">モーダルコンテンツ</div>}
       </div>
    );
 }



Tauri v2との統合

useTauriCommand

Tauri v2では、バックエンドのRustコマンドをフロントエンドから呼び出す時に、invoke 関数を使用する。

useTauriCommand は、この invoke 関数をラップして非同期処理の状態管理を統一するカスタムHookである。

 import { useState, useEffect, useRef } from 'react';
 import { invoke } from '@tauri-apps/api/core';
 
 // Tauriコマンド実行結果の型定義
 // T: コマンドの戻り値の型
 interface UseTauriCommandResult<T> {
    result: T | null;      // コマンド実行結果 (成功時)
    loading: boolean;      // ロード中フラグ
    error: string | null;  // エラーメッセージ (失敗時)
 }
 
 // useTauriCommand フックのオプション型定義
 // A: コマンド引数の型
 interface UseTauriCommandOptions<A> {
    command: string;          // 呼び出すRustコマンド名
    args?: A;                 // コマンドに渡す引数 (オプション)
    shouldExecute?: boolean;  // 実行可否フラグ (デフォルト: true)
 }
 
 /**
  * TauriのRustコマンドを非同期で呼び出すカスタムフック
  * ローディング状態とエラーハンドリングを統一的に管理する
  * 
  * @template T    - コマンドの戻り値の型
  * @template A    - コマンド引数の型 (デフォルト: Record<string, unknown>)
  * @param options - コマンド名、引数、実行フラグを含むオプションオブジェクト
  * @returns 結果、ローディング状態、エラーメッセージを含むオブジェクト
  */
 function useTauriCommand<T, A = Record<string, unknown>>(
    options: UseTauriCommandOptions<A>
 ): UseTauriCommandResult<T> {
    const { command, args, shouldExecute = true } = options;  // オプションから各プロパティを取り出す (shouldExecuteはデフォルトでtrue)
    const [result, setResult] = useState<T | null>(null);     // コマンド実行結果を管理する状態
    const [loading, setLoading] = useState<boolean>(false);   // ローディング状態を管理
    const [error, setError] = useState<string | null>(null);  // エラーメッセージを管理
    const isMounted = useRef<boolean>(true);                  // コンポーネントのマウント状態を追跡 (メモリリーク防止用)
 
    // マウント状態を管理するエフェクト
    // クリーンアップ関数でアンマウントを検知
    useEffect(() => {
       isMounted.current = true;
       return () => {
          isMounted.current = false;
       };
    }, []);
 
    // コマンド実行のメイン処理
    useEffect(() => {
       // 実行フラグがfalseの場合は処理をスキップ
       if (!shouldExecute) return;
 
       // 非同期でコマンドを実行する内部関数
       const execute = async () => {
          setLoading(true);  // ローディング開始
          setError(null);    // エラーをクリア
 
          try {
             const data = await invoke<T>(command, args as Record<string, unknown>);  // Tauriのinvoke関数でRustコマンドを呼び出し
             // マウント中の場合のみ結果をセット (アンマウント後のstate更新を防止)
             if (isMounted.current) setResult(data);
          }
          catch (err) {
             // エラーハンドリング : マウント中の場合のみエラーをセット
             if (isMounted.current) {
                setError(err instanceof Error ? err.message : String(err));
             }
          }
          finally {
             // 成功・失敗に関わらずローディング状態を解除
             if (isMounted.current) setLoading(false);
          }
       };
 
       execute();
    }, [command, shouldExecute]);  // command または shouldExecuteが変更されたら再実行
 
    // 結果をオブジェクトとして返す
    return { result, loading, error };
 }
 
 // 使用例 : Rustコマンド "get_system_info" を呼び出す
 function SystemInfo() {
    // useTauriCommandフックを使用してシステム情報を取得
    const { result, loading, error } = useTauriCommand<string>({
       command: 'get_system_info',
       shouldExecute: true,
    });
 
    // ローディング中の表示
    if (loading) return <p>読み込み中...</p>;
    // エラー時の表示
    if (error) return <p>エラー: {error}</p>;
    // 結果の表示
    return <p>{result}</p>;
 }



カスタムHookのテスト

カスタムHookは @testing-library/reactrenderHook 関数を使用してテストする。
コンポーネントを経由せずにHookの動作を直接検証できる。

  • useCounterのテスト
     import { renderHook, act } from '@testing-library/react';
     import { useCounter } from './useCounter';
     
     // useCounterフックのテストスイート
     describe('useCounter', () => {
        // テスト1 : 初期値が正しく設定されることを検証
        it('初期値が設定されること', () => {
           // renderHookでHookをテスト環境で実行
           // 引数として初期値10を渡す
           const { result } = renderHook(() => useCounter(10));
           
           // result.currentでHookの戻り値にアクセス
           // タプルの1つ目の要素が現在のカウント値
           expect(result.current[0]).toBe(10);
        });
     
        // テスト2 : increment関数で値が1増加することを検証
        it('incrementで値が1増加すること', () => {
           const { result } = renderHook(() => useCounter(0));  // 初期値0でHookを実行
           const [, increment] = result.current;                // タプルの2つ目の要素がincrement関数
     
           // act関数内で状態更新を伴う操作を実行
           // Reactの状態更新を適切に処理するために必須
           act(() => {
              increment();
           });
     
           // 増加後の値を検証
           expect(result.current[0]).toBe(1);
        });
     
        // テスト3 : decrement関数で値が1減少することを検証
        it('decrementで値が1減少すること', () => {
           const { result } = renderHook(() => useCounter(5));  // 初期値5でHookを実行
           const [, , decrement] = result.current;              // タプルの3つ目の要素がdecrement関数
     
           // act関数内でdecrementを実行
           act(() => {
              decrement();
           });
     
           // 減少後の値を検証
           expect(result.current[0]).toBe(4);
        });
     });
    

  • useFetchのテスト (外部依存のMock化)
    fetch 等の外部依存をMockに差し替えてテストする場合の例
     import { renderHook, waitFor } from '@testing-library/react';
     import { useFetch } from './useFetch';
     
     // useFetchフックのテストスイート
     describe('useFetch', () => {
        // 各テストの前に実行: fetchをMock関数に差し替え
        beforeEach(() => {
           // グローバルのfetchをjest.fn()でモック化
           // 実際のネットワークリクエストを防止
           global.fetch = jest.fn();
        });
     
        // 各テストの後に実行 : モックをリセット
        afterEach(() => {
           // 全てのモックの状態をクリア
           // テスト間の干渉を防止
           jest.resetAllMocks();
        });
     
        // テスト : データを正常に取得できることを検証
        it('データを正常に取得すること', async () => {
           const mockData = { id: 1, name: 'テストユーザ' };  // モックデータを定義
           
           // fetchのモック実装を設定
           // mockResolvedValueOnceで1回だけ解決されるPromiseを返す
           (global.fetch as jest.Mock).mockResolvedValueOnce({
              ok: true,                                     // HTTP成功ステータスをシミュレート
              json: async () => mockData,                   // JSONパース結果をモック
           });
     
           // Hookを実行 (型引数でモックデータの型を指定)
           const { result } = renderHook(() => useFetch<typeof mockData>('/api/user'));
     
           // waitForで非同期処理の完了を待機
           // ローディングが終了するまで待つ
           await waitFor(() => {
              expect(result.current.loading).toBe(false);
           });
     
           // 取得したデータがモックデータと一致することを検証
           expect(result.current.data).toEqual(mockData);
     
           // エラーが発生していないことを検証
           expect(result.current.error).toBeNull();
        });
     });
    



設計原則

単一責任の原則

1つのカスタムHookは、1つの責任のみを持つように設計する。

複数の機能を1つのHookに詰め込むと、再利用性が下がりテストも困難になる。

 // 正しい : 責任を分割する
 function useUser(userId: string) {
    /* ... */
 }
 
 function useUserPosts(userId: string) {
    /* ... */
 }
 
 // 誤り : 複数の責任が混在している
 function useUserAndPosts() {
    const [user, setUser]   = useState(null);
    const [posts, setPosts] = useState([]);
    // ...略
 }


依存配列の完全性

useEffectuseCallbackuseMemo の依存配列には、内部で参照する全ての値を含める。
ESLintの react-hooks/exhaustive-deps ルールを有効にすることにより、依存配列の漏れを自動的に検出できる。

 // 正しい : 参照する全ての値を依存配列に含める
 useEffect(() => {
    fetchData(query);
 }, [query]);
 
 // 誤り : queryが依存配列に含まれていない
 useEffect(() => {
    fetchData(query);
 }, []); // queryの変化が反映されないバグが発生する


クリーンアップの徹底

イベントリスナーの登録、タイマの設定、非同期処理の実行等、副作用を伴う処理には必ずクリーンアップ関数を定義する。

クリーンアップを怠るとメモリリークやアンマウント後のステート更新エラーの原因となる。

 useEffect(() => {
    const timerId = setInterval(() => {
       setCount((prev) => prev + 1);
    }, 1000);
 
    // クリーンアップ : コンポーネントのアンマウント時にタイマを解除する
    return () => clearInterval(timerId);
 }, []);



よくある間違い

useプレフィックスの欠落

use プレフィックスを省略した関数内でHookを呼び出した場合、ESLintが規則違反を検出できなくなる。

 // 正しい
 function useOnlineStatus() {
    const [isOnline, setIsOnline] = useState(true);
    return isOnline;
 }
 
 // 誤り : useプレフィックスがないため、ESLintがHooksルールを検証しない
 function getOnlineStatus() {
    const [isOnline, setIsOnline] = useState(true); // 規則違反を検出できない
    return isOnline;
 }


条件付き呼び出し

Hookをif文やループ、早期returnの後で呼び出してはならない。
Reactは呼び出し順序に基づいてHookの状態を管理しているため、条件分岐で呼び出し順序が変わるとステートの対応関係が崩れる。

 // 正しい : Hookは常に無条件で呼び出す
 function useConditionalHook(condition: boolean) {
    const [value, setValue] = useState(0);
    // 条件はHookの呼び出し後に使用する
    const displayValue = condition ? value : null;
    return displayValue;
 }
 
 // 誤り : 条件付きでHookを呼び出している
 function useConditionalHook(condition: boolean) {
    if (condition) {
       const [value, setValue] = useState(0); // Hooksのルール違反
    }
    // ...略
 }


クリーンアップ忘れ

イベントリスナーや非同期処理のクリーンアップを忘れると、コンポーネントのアンマウント後にもステート更新が実行されてメモリリークが発生する。

 // 正しい : クリーンアップ関数を返す
 useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
 }, []);
 
 // 誤り : クリーンアップがない
 useEffect(() => {
    window.addEventListener('resize', handleResize);
    // クリーンアップがないため、コンポーネントのアンマウント後もリスナーが残る
 }, []);



関連情報