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 |
省略可能。フォーマッタ関数。コストの高い処理を遅延実行するために使用 |
| 返り値 | 説明 |
|---|---|
| なし |
useDebugValue は undefined を返す |
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
export function useDebugValue<T>(
value: T,
formatterFn: ?(value: T) => mixed,
): void {
if (__DEV__) {
const dispatcher = resolveDispatcher();
return dispatcher.useDebugValue(value, formatterFn);
}
}
ポイント解説
-
__DEV__チェック: 開発環境でのみ処理が実行される -
formatterFn: オプションのフォーマッタ関数で、遅延評価に使用 - 本番環境: 関数の本体が空になり、完全に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',
});
}
処理の流れ
- hookLog に追加: フック情報をログ配列にプッシュ
-
formatterFn の実行: フォーマッタ関数が渡されていれば、その結果を
valueに設定 - 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);
}
}
}
処理の詳細
-
DebugValue ノードの収集: フックツリーから
DebugValueタイプのノードを抽出 - 親フックへの関連付け: 収集したデバッグ値を親のカスタムフックに割り当て
- 複数のデバッグ値: 複数ある場合は配列として表示
- カスタムフック外での使用: 親がない場合は無視される
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 デバッグ値が表示されない場合
確認事項:
-
カスタムフック内で呼び出しているか
-
useで始まる関数内で呼び出す必要がある
-
-
React DevTools がインストールされているか
- ブラウザ拡張機能として React DevTools が必要
-
開発モードで実行しているか
- 本番ビルドでは
useDebugValueは無視される
- 本番ビルドでは
-
コンポーネントをインスペクトしているか
- 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
❌ おそらく不要
参考文献: