Skip to main content
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

value
T
required
The value you want to display in React DevTools. It can be of any type.
format
(value: T) => mixed
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>;
}

Deferring formatting of a debug value

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 };
}

Media query hooks

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];
}

Use formatting for expensive operations

// ✅ 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

I don’t see the debug value in DevTools

Make sure:
  1. You’re in development mode (doesn’t work in production)
  2. You’re using React DevTools browser extension
  3. You called useDebugValue inside a custom Hook (not a component)
  4. The component using your Hook is selected in DevTools

The formatting function runs on every render

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 };
}