The useDebugValue hook displays custom labels for hooks in React DevTools. Here's how to use it effectively.
Basic Usage#
1import { useDebugValue, useState } from 'react';
2
3function useOnlineStatus() {
4 const [isOnline, setIsOnline] = useState(true);
5
6 // Display in DevTools
7 useDebugValue(isOnline ? 'Online' : 'Offline');
8
9 return isOnline;
10}
11
12// In DevTools: OnlineStatus: "Online"With Custom Hooks#
1import { useDebugValue, useEffect, useState } from 'react';
2
3function useLocalStorage<T>(key: string, initialValue: T) {
4 const [value, setValue] = useState<T>(() => {
5 const stored = localStorage.getItem(key);
6 return stored ? JSON.parse(stored) : initialValue;
7 });
8
9 useEffect(() => {
10 localStorage.setItem(key, JSON.stringify(value));
11 }, [key, value]);
12
13 // Show key and value in DevTools
14 useDebugValue(`${key}: ${JSON.stringify(value)}`);
15
16 return [value, setValue] as const;
17}
18
19// In DevTools: LocalStorage: "theme: \"dark\""Deferred Formatting#
1import { useDebugValue, useState } from 'react';
2
3interface User {
4 id: string;
5 name: string;
6 email: string;
7 lastLogin: Date;
8}
9
10function useUser(userId: string) {
11 const [user, setUser] = useState<User | null>(null);
12
13 // Expensive formatting only runs when DevTools is open
14 useDebugValue(user, (user) => {
15 if (!user) return 'No user';
16 return `${user.name} (${user.email}) - Last: ${user.lastLogin.toLocaleDateString()}`;
17 });
18
19 return user;
20}
21
22// Formatting function only called when inspecting in DevToolsMultiple Debug Values#
1import { useDebugValue, useEffect, useState } from 'react';
2
3interface FetchState<T> {
4 data: T | null;
5 loading: boolean;
6 error: Error | null;
7}
8
9function useFetch<T>(url: string) {
10 const [state, setState] = useState<FetchState<T>>({
11 data: null,
12 loading: true,
13 error: null,
14 });
15
16 useEffect(() => {
17 const controller = new AbortController();
18
19 fetch(url, { signal: controller.signal })
20 .then((res) => res.json())
21 .then((data) => setState({ data, loading: false, error: null }))
22 .catch((error) => {
23 if (error.name !== 'AbortError') {
24 setState({ data: null, loading: false, error });
25 }
26 });
27
28 return () => controller.abort();
29 }, [url]);
30
31 // Show comprehensive state in DevTools
32 useDebugValue(state, ({ data, loading, error }) => {
33 if (loading) return 'Loading...';
34 if (error) return `Error: ${error.message}`;
35 return `Loaded: ${JSON.stringify(data).slice(0, 50)}...`;
36 });
37
38 return state;
39}Authentication Hook#
1import { useDebugValue, useEffect, useState } from 'react';
2
3interface AuthState {
4 user: User | null;
5 isAuthenticated: boolean;
6 isLoading: boolean;
7}
8
9function useAuth() {
10 const [state, setState] = useState<AuthState>({
11 user: null,
12 isAuthenticated: false,
13 isLoading: true,
14 });
15
16 useDebugValue(state, ({ user, isAuthenticated, isLoading }) => {
17 if (isLoading) return 'Checking auth...';
18 if (!isAuthenticated) return 'Not authenticated';
19 return `Authenticated as ${user?.name}`;
20 });
21
22 // Auth logic...
23
24 return state;
25}
26
27// In DevTools: Auth: "Authenticated as John Doe"Form State Hook#
1import { useDebugValue, useState } from 'react';
2
3interface FormState<T> {
4 values: T;
5 errors: Partial<Record<keyof T, string>>;
6 touched: Partial<Record<keyof T, boolean>>;
7 isValid: boolean;
8 isDirty: boolean;
9}
10
11function useForm<T extends Record<string, any>>(initialValues: T) {
12 const [state, setState] = useState<FormState<T>>({
13 values: initialValues,
14 errors: {},
15 touched: {},
16 isValid: true,
17 isDirty: false,
18 });
19
20 useDebugValue(state, ({ isValid, isDirty, errors }) => {
21 const errorCount = Object.keys(errors).length;
22 return `${isDirty ? 'Dirty' : 'Clean'}, ${isValid ? 'Valid' : `${errorCount} errors`}`;
23 });
24
25 // Form logic...
26
27 return state;
28}
29
30// In DevTools: Form: "Dirty, 2 errors"Timer Hook#
1import { useDebugValue, useEffect, useState } from 'react';
2
3function useTimer(initialTime: number) {
4 const [time, setTime] = useState(initialTime);
5 const [isRunning, setIsRunning] = useState(false);
6
7 useEffect(() => {
8 if (!isRunning) return;
9
10 const interval = setInterval(() => {
11 setTime((t) => Math.max(0, t - 1));
12 }, 1000);
13
14 return () => clearInterval(interval);
15 }, [isRunning]);
16
17 useDebugValue({ time, isRunning }, ({ time, isRunning }) => {
18 const minutes = Math.floor(time / 60);
19 const seconds = time % 60;
20 const formatted = `${minutes}:${seconds.toString().padStart(2, '0')}`;
21 return `${formatted} (${isRunning ? 'Running' : 'Paused'})`;
22 });
23
24 return { time, isRunning, start: () => setIsRunning(true), pause: () => setIsRunning(false) };
25}
26
27// In DevTools: Timer: "5:30 (Running)"WebSocket Hook#
1import { useDebugValue, useEffect, useRef, useState } from 'react';
2
3type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'error';
4
5function useWebSocket(url: string) {
6 const [state, setState] = useState<ConnectionState>('connecting');
7 const [messageCount, setMessageCount] = useState(0);
8 const wsRef = useRef<WebSocket | null>(null);
9
10 useDebugValue({ state, messageCount }, ({ state, messageCount }) => {
11 return `${state} (${messageCount} messages)`;
12 });
13
14 useEffect(() => {
15 const ws = new WebSocket(url);
16 wsRef.current = ws;
17
18 ws.onopen = () => setState('connected');
19 ws.onclose = () => setState('disconnected');
20 ws.onerror = () => setState('error');
21 ws.onmessage = () => setMessageCount((c) => c + 1);
22
23 return () => ws.close();
24 }, [url]);
25
26 return { state, messageCount, ws: wsRef.current };
27}
28
29// In DevTools: WebSocket: "connected (42 messages)"Media Query Hook#
1import { useDebugValue, useEffect, useState } from 'react';
2
3function useMediaQuery(query: string) {
4 const [matches, setMatches] = useState(() => {
5 if (typeof window === 'undefined') return false;
6 return window.matchMedia(query).matches;
7 });
8
9 useEffect(() => {
10 const mediaQuery = window.matchMedia(query);
11 const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
12
13 mediaQuery.addEventListener('change', handler);
14 return () => mediaQuery.removeEventListener('change', handler);
15 }, [query]);
16
17 useDebugValue(`${query}: ${matches}`);
18
19 return matches;
20}
21
22// In DevTools: MediaQuery: "(min-width: 768px): true"Composed Hooks#
1import { useDebugValue } from 'react';
2
3function useUserPreferences(userId: string) {
4 const { data: user } = useFetch<User>(`/api/users/${userId}`);
5 const [theme] = useLocalStorage('theme', 'light');
6 const [language] = useLocalStorage('language', 'en');
7 const isOnline = useOnlineStatus();
8
9 // Debug label for composed hook
10 useDebugValue({
11 user: user?.name,
12 theme,
13 language,
14 isOnline,
15 }, (state) => {
16 return `${state.user || 'Loading'} | ${state.theme} | ${state.language} | ${state.isOnline ? '🟢' : '🔴'}`;
17 });
18
19 return { user, theme, language, isOnline };
20}
21
22// In DevTools: UserPreferences: "John | dark | en | 🟢"Conditional Debug Value#
1import { useDebugValue, useState } from 'react';
2
3function useFeatureFlag(flagName: string) {
4 const [enabled, setEnabled] = useState(false);
5 const [loaded, setLoaded] = useState(false);
6
7 // Only show debug value in development
8 if (process.env.NODE_ENV === 'development') {
9 // eslint-disable-next-line react-hooks/rules-of-hooks
10 useDebugValue(
11 { flagName, enabled, loaded },
12 ({ flagName, enabled, loaded }) => {
13 if (!loaded) return `${flagName}: loading...`;
14 return `${flagName}: ${enabled ? '✓ enabled' : '✗ disabled'}`;
15 }
16 );
17 }
18
19 return { enabled, loaded };
20}Complex State Debugging#
1import { useDebugValue, useReducer } from 'react';
2
3interface State {
4 items: Item[];
5 selectedId: string | null;
6 filter: string;
7 sortBy: 'name' | 'date' | 'size';
8 sortOrder: 'asc' | 'desc';
9}
10
11function useItemManager(initialItems: Item[]) {
12 const [state, dispatch] = useReducer(reducer, {
13 items: initialItems,
14 selectedId: null,
15 filter: '',
16 sortBy: 'name',
17 sortOrder: 'asc',
18 });
19
20 useDebugValue(state, (s) => {
21 const filtered = s.filter ? `filtered "${s.filter}"` : 'unfiltered';
22 const sorted = `sorted by ${s.sortBy} ${s.sortOrder}`;
23 const selected = s.selectedId ? `selected: ${s.selectedId}` : 'none selected';
24 return `${s.items.length} items, ${filtered}, ${sorted}, ${selected}`;
25 });
26
27 return { state, dispatch };
28}
29
30// In DevTools: ItemManager: "42 items, filtered "test", sorted by name asc, selected: item-123"Best Practices#
When to Use:
✓ Custom hooks in libraries
✓ Complex hook state
✓ Debugging during development
✓ Team collaboration
Format Function:
✓ Use for expensive formatting
✓ Keep formatting simple
✓ Return readable strings
✓ Handle null/undefined
Content:
✓ Show relevant state
✓ Use concise labels
✓ Include counts/status
✓ Format dates/objects
Avoid:
✗ Using in every hook
✗ Complex computations
✗ Sensitive data
✗ Production overhead
Conclusion#
useDebugValue enhances custom hooks with descriptive labels in React DevTools. Use the deferred formatting function for expensive operations to avoid performance impact. Focus on displaying the most relevant state information that helps during debugging. It's most valuable for reusable hooks in libraries or complex application hooks that benefit from clear state visibility.