useDebugValue is a React Hook that lets you add a label to a custom Hook in React DevTools.
function useDebugValue<T>(
value: T,
format?: (value: T) => mixed
): void
useDebugValue only runs in development mode and has no effect in production.
Parameters
The value you want to display in React DevTools. It can be of any type.
Optional formatting function. When the Hook is inspected, React DevTools will call the formatting function with the value as the argument, and then display the returned formatted value.This is useful if formatting is expensive - it will only be called when the Hook is actually inspected in DevTools.
Returns
useDebugValue returns undefined.
Usage
Adding a label to a custom Hook
Call useDebugValue at the top level of your custom Hook to display a readable debug value:
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);
};
}, []);
// Show "Online" or "Offline" in DevTools
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
// When you inspect a component using this Hook in DevTools:
function StatusIndicator() {
const isOnline = useOnlineStatus(); // Shows: "Online" or "Offline"
return <div>{isOnline ? '🟢' : '🔴'}</div>;
}
If formatting the value is expensive, pass a formatting function as the second argument:
function useDate() {
const [date, setDate] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setDate(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
// Format function only runs when inspected in DevTools
useDebugValue(date, date => {
// Expensive formatting
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
});
return date;
}
Without a format function, React would call date.toLocaleString() on every render. With a format function, it only calls it when you inspect the Hook in DevTools.
Common Use Cases
State management hooks
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
const setValue = value => {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
};
// Show the key and value in DevTools
useDebugValue(`${key}: ${JSON.stringify(storedValue)}`);
return [storedValue, setValue];
}
Network status hooks
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
// Show status in DevTools
useDebugValue(
loading ? 'Loading...' :
error ? `Error: ${error.message}` :
'Success'
);
return { data, loading, error };
}
Complex state hooks
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
// Show form state summary in DevTools
useDebugValue(values, values => {
const fieldCount = Object.keys(values).length;
const errorCount = Object.keys(errors).length;
return `${fieldCount} fields, ${errorCount} errors`;
});
// ... rest of hook logic
return { values, errors, touched, setValues, setErrors, setTouched };
}
function useMediaQuery(query) {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
// Show query and result in DevTools
useDebugValue(`${query}: ${matches ? 'matches' : 'no match'}`);
return matches;
}
Timer hooks
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
// Show interval status
useDebugValue(
delay === null ? 'Paused' : `Running every ${delay}ms`
);
}
TypeScript
import { useDebugValue, useState } from 'react';
// Basic usage
function useCustomHook(): string {
const [value, setValue] = useState('initial');
useDebugValue(value);
return value;
}
// With formatter
function useDateHook(): Date {
const [date, setDate] = useState(new Date());
useDebugValue(date, (d: Date): string => {
return d.toISOString();
});
return date;
}
// Complex type
interface User {
id: string;
name: string;
email: string;
}
function useUser(userId: string): User | null {
const [user, setUser] = useState<User | null>(null);
useDebugValue(user, (u: User | null): string => {
return u ? `User: ${u.name} (${u.email})` : 'No user';
});
// ... fetch user logic
return user;
}
Generic custom hook
function useAsync<T>(asyncFunction: () => Promise<T>) {
const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
useDebugValue(status, (s) => {
switch (s) {
case 'idle': return '⚪ Idle';
case 'pending': return '🟡 Loading...';
case 'success': return '🟢 Success';
case 'error': return '🔴 Error';
}
});
// ... async logic
return { status, data, error };
}
Best Practices
Keep it simple
// ✅ Good: Simple, readable label
function useCounter(initialCount) {
const [count, setCount] = useState(initialCount);
useDebugValue(count);
return [count, setCount];
}
// ❌ Bad: Too verbose
function useCounter(initialCount) {
const [count, setCount] = useState(initialCount);
useDebugValue(`Counter Hook initialized with ${initialCount}, current value is ${count}, setter function available`);
return [count, setCount];
}
// ✅ Good: Defers expensive formatting
function useData(data) {
useDebugValue(data, d => JSON.stringify(d, null, 2));
return data;
}
// ❌ Bad: Formats on every render
function useData(data) {
useDebugValue(JSON.stringify(data, null, 2));
return data;
}
Only use in custom hooks
// ✅ Good: In custom Hook
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useDebugValue(size, s => `${s.width}x${s.height}`);
return size;
}
// ❌ Bad: In component (has no effect)
function Component() {
const [count, setCount] = useState(0);
useDebugValue(count); // Won't show up in DevTools
return <div>{count}</div>;
}
Provide meaningful labels
// ✅ Good: Meaningful label
function useAuth() {
const [user, setUser] = useState(null);
useDebugValue(user ? `Logged in as ${user.name}` : 'Not logged in');
return { user, setUser };
}
// ❌ Bad: Uninformative label
function useAuth() {
const [user, setUser] = useState(null);
useDebugValue(user); // Just shows the object
return { user, setUser };
}
Troubleshooting
Make sure:
- You’re in development mode (doesn’t work in production)
- You’re using React DevTools browser extension
- You called
useDebugValue inside a custom Hook (not a component)
- The component using your Hook is selected in DevTools
Make sure you’re passing the formatter as a second argument:
// ❌ Runs on every render
useDebugValue(expensiveFormat(value));
// ✅ Only runs when inspected
useDebugValue(value, expensiveFormat);
Should I add useDebugValue to all my hooks?
No! Only add it when:
- The Hook is part of a shared library
- The Hook’s state is complex or not obvious
- You’re debugging and need visibility
Don’t add it to:
- Simple hooks that just wrap useState
- Hooks that are only used once
- Hooks where the value is obvious
Examples
Authentication hook
function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useDebugValue(user, user => {
if (loading) return 'Loading...';
if (!user) return 'Not authenticated';
return `${user.name} (${user.role})`;
});
// ... auth logic
return { user, loading };
}
API hook
function useAPI(endpoint) {
const [data, setData] = useState(null);
const [status, setStatus] = useState('idle');
useDebugValue(`${endpoint} [${status}]`);
// ... fetch logic
return { data, status };
}
Animation hook
function useAnimation(duration) {
const [progress, setProgress] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useDebugValue(
isRunning ? `Running: ${Math.round(progress * 100)}%` : 'Idle'
);
// ... animation logic
return { progress, isRunning };
}