0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React Hooks を Hackしよう!【Part16: useDebugValue完全ガイド】

Posted at

React DevToolsでカスタムフックをデバッグする際、「このフックの内部状態は何?」と確認したいことがあります。useDebugValue は、そんな開発体験を向上させるためのフックです。

1. useDebugValue とは

1.1 React DevTools とは

useDebugValue を理解するために、まず React DevTools について簡単に説明します。

React DevTools
React DevTools は、React アプリケーションをデバッグするための公式ブラウザ拡張機能です。Chrome、Firefox、Edge などのブラウザで利用できます。

主な機能:

  • Components タブ: コンポーネントツリーの確認、props/state の表示・編集
  • Profiler タブ: レンダリングパフォーマンスの計測・分析
  • フックのインスペクト: カスタムフックを含むすべてのフックの状態確認

インストール方法:

React DevTools でコンポーネントを選択すると、そのコンポーネントが使用しているフックの一覧が表示されます。useDebugValue は、このフック一覧にカスタムラベルを追加するためのフックです。

1.2 基本概念

useDebugValue は、カスタムフック内で使用することで、React DevTools にカスタムフックの「デバッグ値」を表示できるフックです。

import { useDebugValue, useState } from 'react';

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  
  // DevTools に 'Online' または 'Offline' と表示される
  useDebugValue(isOnline ? 'Online' : 'Offline');
  
  return isOnline;
}

React DevTools でこのフックを使用しているコンポーネントを調べると、OnlineStatus: "Online" のようなラベルが表示されます。

1.3 useDebugValue の API

useDebugValue(value, format?)
引数 説明
value DevTools に表示したい値。任意の型を使用可能
format 省略可能。フォーマッタ関数。コストの高い処理を遅延実行するために使用
返り値 説明
なし useDebugValueundefined を返す

1.4 他のフックとの違い

特徴 useDebugValue その他のフック(useState等)
実行環境 開発環境のみ 開発・本番両方
目的 デバッグ支援 アプリケーションロジック
返り値 なし 状態や関数を返す
DevTools 連携 ラベルを表示 状態を表示
本番への影響 完全に無視される 動作に影響

注意点
useDebugValue はカスタムフックのトップレベルでのみ呼び出してください。通常のコンポーネント内で直接使用しても、DevTools には表示されません。

2. useDebugValue の内部構造を徹底解剖

useDebugValue の内部実装は、他のフックと比較して非常にシンプルです。なぜなら、本番環境では何もしないという設計だからです。

2.0 全体像: useDebugValue の動作原理

🎣 useDebugValue(フック呼び出し)
   ↓
🔍 開発環境チェック(__DEV__)
   ↓
   ├── 本番環境: 何もしない(no-op)
   │
   └── 開発環境:
       ↓
       📝 react-debug-tools が独自実装を注入
       ↓
       🖥️ DevTools が hookLog に値を追加
       ↓
       🏷️ カスタムフックにラベルとして表示

重要なポイント: useDebugValue は本番環境では完全に空の関数であり、パフォーマンスへの影響はありません!

2.1 エントリポイント: packages/react/src/ReactHooks.js

引用元: packages/react/src/ReactHooks.js (L160-L167)

// packages/react/src/ReactHooks.js

export function useDebugValue<T>(
  value: T,
  formatterFn: ?(value: T) => mixed,
): void {
  if (__DEV__) {
    const dispatcher = resolveDispatcher();
    return dispatcher.useDebugValue(value, formatterFn);
  }
}

ポイント解説

  1. __DEV__ チェック: 開発環境でのみ処理が実行される
  2. formatterFn: オプションのフォーマッタ関数で、遅延評価に使用
  3. 本番環境: 関数の本体が空になり、完全にno-op(何もしない操作)

2.2 Reconciler での実装: packages/react-reconciler/src/ReactFiberHooks.js

引用元: packages/react-reconciler/src/ReactFiberHooks.js (L2888-L2894)

// packages/react-reconciler/src/ReactFiberHooks.js

function mountDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void {
  // This hook is normally a no-op.
  // The react-debug-hooks package injects its own implementation
  // so that e.g. DevTools can display custom hook values.
}

const updateDebugValue = mountDebugValue;

驚くほどシンプルな実装

コメントにもあるように、mountDebugValue意図的に空の関数として定義されています。実際の処理は react-debug-tools パッケージが注入します。

2.3 DevTools との連携: packages/react-debug-tools/src/ReactDebugHooks.js

引用元: packages/react-debug-tools/src/ReactDebugHooks.js (L421-L429)

// packages/react-debug-tools/src/ReactDebugHooks.js

function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
  hookLog.push({
    displayName: null,
    primitive: 'DebugValue',
    stackError: new Error(),
    value: typeof formatterFn === 'function' ? formatterFn(value) : value,
    debugInfo: null,
    dispatcherHookName: 'DebugValue',
  });
}

処理の流れ

  1. hookLog に追加: フック情報をログ配列にプッシュ
  2. formatterFn の実行: フォーマッタ関数が渡されていれば、その結果を value に設定
  3. stackError: デバッグ用のスタックトレースを保持

2.4 デバッグ値のカスタムフックへの関連付け

引用元: packages/react-debug-tools/src/ReactDebugHooks.js (L1133-L1163)

// packages/react-debug-tools/src/ReactDebugHooks.js

// Custom hooks support user-configurable labels (via the special useDebugValue() hook).
// That hook adds user-provided values to the hooks tree,
// but these values aren't intended to appear alongside of the other hooks.
// Instead they should be attributed to their parent custom hook.
// This method walks the tree and assigns debug values to their custom hook owners.
function processDebugValues(
  hooksTree: HooksTree,
  parentHooksNode: HooksNode | null,
): void {
  const debugValueHooksNodes: Array<HooksNode> = [];

  for (let i = 0; i < hooksTree.length; i++) {
    const hooksNode = hooksTree[i];
    if (hooksNode.name === 'DebugValue' && hooksNode.subHooks.length === 0) {
      hooksTree.splice(i, 1);
      i--;
      debugValueHooksNodes.push(hooksNode);
    } else {
      processDebugValues(hooksNode.subHooks, hooksNode);
    }
  }

  // Bubble debug value labels to their custom hook owner.
  // If there is no parent hook, just ignore them for now.
  if (parentHooksNode !== null) {
    if (debugValueHooksNodes.length === 1) {
      parentHooksNode.value = debugValueHooksNodes[0].value;
    } else if (debugValueHooksNodes.length > 1) {
      parentHooksNode.value = debugValueHooksNodes.map(({value}) => value);
    }
  }
}

処理の詳細

  1. DebugValue ノードの収集: フックツリーから DebugValue タイプのノードを抽出
  2. 親フックへの関連付け: 収集したデバッグ値を親のカスタムフックに割り当て
  3. 複数のデバッグ値: 複数ある場合は配列として表示
  4. カスタムフック外での使用: 親がない場合は無視される

2.5 内部構造のまとめ図

3. 基本的な使い方

3.1 シンプルな使用例

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

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  // DevTools に状態を分かりやすく表示
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

3.2 フォーマッタ関数の使用

コストの高いフォーマット処理がある場合、フォーマッタ関数を使って遅延評価できます:

import { useDebugValue, useState } from 'react';

function useFormattedDate(date: Date) {
  const [currentDate] = useState(date);

  // フォーマッタ関数を使用して遅延評価
  // コンポーネントがインスペクトされた時のみ実行される
  useDebugValue(currentDate, (date) => date.toLocaleDateString('ja-JP', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    weekday: 'long',
  }));

  return currentDate;
}

なぜフォーマッタ関数が必要か?

// ❌ 非推奨: 毎回のレンダーでフォーマット処理が実行される
useDebugValue(date.toLocaleDateString('ja-JP', { ... }));

// ✅ 推奨: インスペクト時のみフォーマット処理が実行される
useDebugValue(date, (d) => d.toLocaleDateString('ja-JP', { ... }));

3.3 複数のデバッグ値

一つのカスタムフック内で複数の useDebugValue を呼び出すことができます:

function useUserStatus(userId: string) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  // 複数のデバッグ値を設定
  useDebugValue(isLoading ? 'Loading...' : 'Loaded');
  useDebugValue(user?.name ?? 'No user');
  useDebugValue(error ? `Error: ${error.message}` : 'No error');

  // ... フェッチロジック

  return { user, isLoading, error };
}

DevTools では配列として表示されます:["Loaded", "John Doe", "No error"]

4. ユースケース

4.1 非同期データフェッチのデバッグ

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

type FetchStatus = 'idle' | 'loading' | 'success' | 'error';

function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [status, setStatus] = useState<FetchStatus>('idle');

  useEffect(() => {
    let cancelled = false;
    
    async function fetchData() {
      setStatus('loading');
      try {
        const response = await fetch(url);
        const json = await response.json();
        if (!cancelled) {
          setData(json);
          setStatus('success');
        }
      } catch (e) {
        if (!cancelled) {
          setStatus('error');
        }
      }
    }

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

  // フェッチ状態を分かりやすく表示
  useDebugValue({ url, status, hasData: data !== null });

  return { data, status };
}

4.2 フォーム状態管理のデバッグ

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

function useFormField(initialValue: string, validator?: (value: string) => string | null) {
  const [value, setValue] = useState(initialValue);
  const [touched, setTouched] = useState(false);
  
  const error = touched && validator ? validator(value) : null;

  const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  }, []);

  const handleBlur = useCallback(() => {
    setTouched(true);
  }, []);

  // フォームフィールドの状態をデバッグ表示
  useDebugValue(
    { value, touched, error },
    ({ value, touched, error }) => 
      `"${value}" ${touched ? '(touched)' : ''} ${error ? `[${error}]` : ''}`
  );

  return { value, error, handleChange, handleBlur };
}

4.3 認証状態のデバッグ

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

interface AuthState {
  user: { id: string; name: string; role: string } | null;
  isAuthenticated: boolean;
  isLoading: boolean;
}

const AuthContext = createContext<AuthState | null>(null);

function useAuth() {
  const auth = useContext(AuthContext);
  
  if (!auth) {
    throw new Error('useAuth must be used within AuthProvider');
  }

  // 認証状態を分かりやすく表示
  useDebugValue(auth, (state) => {
    if (state.isLoading) return '🔄 Loading...';
    if (!state.isAuthenticated) return '🔒 Not authenticated';
    return `✓ ${state.user?.name} (${state.user?.role})`;
  });

  return auth;
}

4.4 WebSocket 接続状態のデバッグ

import { useDebugValue, useState, useEffect, useRef } from 'react';

type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';

function useWebSocket(url: string) {
  const [status, setStatus] = useState<ConnectionStatus>('disconnected');
  const [lastMessage, setLastMessage] = useState<string | null>(null);
  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    const ws = new WebSocket(url);
    wsRef.current = ws;
    setStatus('connecting');

    ws.onopen = () => setStatus('connected');
    ws.onclose = () => setStatus('disconnected');
    ws.onerror = () => setStatus('error');
    ws.onmessage = (e) => setLastMessage(e.data);

    return () => ws.close();
  }, [url]);

  // WebSocket の状態を詳細に表示
  useDebugValue({ status, url, hasMessage: lastMessage !== null }, (state) => {
    const statusIcon = {
      connecting: '🔄',
      connected: '',
      disconnected: '',
      error: '',
    }[state.status];
    return `${statusIcon} ${state.status} - ${state.url}`;
  });

  return { status, lastMessage, ws: wsRef.current };
}

5. ベストプラクティス

5.1 使うべき場面

// ✅ 共有ライブラリのカスタムフック
function useQuery<T>(queryKey: string) {
  // 複雑な内部状態を持つ
  const [data, setData] = useState<T | null>(null);
  const [status, setStatus] = useState('idle');
  
  // ライブラリ利用者にとって有用なデバッグ情報
  useDebugValue({ queryKey, status, hasData: !!data });
  
  return { data, status };
}

// ✅ 複雑な状態を持つカスタムフック
function useMediaQuery(query: string) {
  const [matches, setMatches] = useState(false);
  
  useDebugValue(matches ? `Matches: ${query}` : `Does not match: ${query}`);
  
  return matches;
}

5.2 避けるべき場面

// ❌ シンプルなフックには不要
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  
  // これは不要 - useState の値が既に DevTools で見える
  // useDebugValue(value ? 'On' : 'Off');
  
  return [value, toggle];
}

// ❌ すべてのフックに追加しない
function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  
  // 単純な状態には不要
  // useDebugValue(count);
  
  return { count, increment: () => setCount(c => c + 1) };
}

5.3 フォーマッタ関数の活用

// ✅ コストの高い処理は遅延評価
function useComplexState(data: ComplexData) {
  useDebugValue(data, (d) => {
    // この処理はインスペクト時のみ実行される
    return JSON.stringify(d, null, 2);
  });
}

// ✅ 日付のフォーマット
function useTimestamp() {
  const [timestamp, setTimestamp] = useState(Date.now());
  
  useDebugValue(timestamp, (ts) => new Date(ts).toISOString());
  
  return timestamp;
}

6. トラブルシューティング

6.1 カスタムフック外で使用した場合

// ❌ 問題: コンポーネント内で直接使用
function MyComponent() {
  useDebugValue('This will be ignored'); // DevTools に表示されない
  return <div>Hello</div>;
}

// ✅ 解決策: カスタムフック内で使用
function useMyHook() {
  useDebugValue('This will be displayed');
  return useState(0);
}

function MyComponent() {
  const [count] = useMyHook();
  return <div>{count}</div>;
}

6.2 デバッグ値が表示されない場合

確認事項:

  1. カスタムフック内で呼び出しているか

    • use で始まる関数内で呼び出す必要がある
  2. React DevTools がインストールされているか

    • ブラウザ拡張機能として React DevTools が必要
  3. 開発モードで実行しているか

    • 本番ビルドでは useDebugValue は無視される
  4. コンポーネントをインスペクトしているか

    • DevTools でコンポーネントを選択する必要がある

6.3 フォーマッタ関数でエラーが発生する場合

// ❌ フォーマッタ関数内でエラー
useDebugValue(maybeNull, (value) => value.property); // null の場合エラー

// ✅ null チェックを追加
useDebugValue(maybeNull, (value) => value?.property ?? 'N/A');

7. まとめ

特徴 説明
開発専用 本番環境では完全に無視される
ノーコスト 本番ビルドではno-op(空の関数)
DevTools 連携 カスタムフックにラベルを表示
遅延評価 フォーマッタ関数でインスペクト時のみ処理

使用の判断基準

useDebugValue を追加すべきか?
  ↓
共有ライブラリの一部か? ──Yes──→ ✅ 追加を検討
  ↓ No
内部状態が複雑か? ──Yes──→ ✅ 追加を検討
  ↓ No
調査が困難なデータ構造か? ──Yes──→ ✅ 追加を検討
  ↓ No
❌ おそらく不要

参考文献:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?