Skip to main content
useDebugValue displays a custom label for custom hooks in the Preact DevTools panel. This helps with debugging by providing meaningful information about custom hook state.

Signature

function useDebugValue<T>(
  value: T,
  formatter?: (value: T) => any
): void

Parameters

value
T
required
The value to display in the devtools. This can be any type: string, number, object, etc.
formatter
(value: T) => any
An optional function to format the value before displaying it in devtools. This is useful for expensive formatting operations that should only run when the devtools are open and inspecting the component.

Returns

void

Basic Usage

import { useState } from 'preact/hooks';
import { useDebugValue } from 'preact/hooks';

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

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

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

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

  // Display in devtools
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

function App() {
  const isOnline = useOnlineStatus();
  return <div>{isOnline ? 'Connected' : 'Disconnected'}</div>;
}

With Formatter

function useUserStatus(userId) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  // Formatter only runs when devtools inspect the component
  useDebugValue(user, (u) => {
    if (!u) return 'Loading...';
    return `${u.name} (${u.status})`;
  });

  return user;
}

Custom Hook Debugging

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  // Show the storage key and current value in devtools
  useDebugValue({ key, value: storedValue });

  return [storedValue, setValue];
}

function Component() {
  const [name, setName] = useLocalStorage('name', 'John');
  return <input value={name} onChange={e => setName(e.target.value)} />;
}

Async State Debugging

function useFetch(url) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null
  });

  useEffect(() => {
    let cancelled = false;

    setState({ data: null, loading: true, error: null });

    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setState({ data, loading: false, error: null });
        }
      })
      .catch(error => {
        if (!cancelled) {
          setState({ data: null, loading: false, error });
        }
      });

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

  // Display fetch status in devtools
  useDebugValue(
    state,
    ({ loading, error, data }) => {
      if (loading) return 'Loading...';
      if (error) return `Error: ${error.message}`;
      if (data) return `Loaded ${Object.keys(data).length} keys`;
      return 'Idle';
    }
  );

  return state;
}

Complex State Debugging

function useForm(initialValues, validate) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Format complex state for devtools
  useDebugValue(
    { values, errors, touched, isSubmitting },
    (state) => {
      const errorCount = Object.keys(state.errors).length;
      const touchedCount = Object.keys(state.touched).length;
      const status = state.isSubmitting ? 'submitting' : 'idle';

      return `Form: ${status}, ${errorCount} errors, ${touchedCount} touched`;
    }
  );

  return {
    values,
    errors,
    touched,
    isSubmitting,
    setValues,
    setErrors,
    setTouched,
    setIsSubmitting
  };
}

Timer Debugging

function useTimer(initialTime = 0) {
  const [time, setTime] = useState(initialTime);
  const [isRunning, setIsRunning] = useState(false);

  useEffect(() => {
    if (!isRunning) return;

    const interval = setInterval(() => {
      setTime(t => t + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, [isRunning]);

  // Format time as MM:SS in devtools
  useDebugValue(time, (t) => {
    const minutes = Math.floor(t / 60);
    const seconds = t % 60;
    return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  });

  return {
    time,
    isRunning,
    start: () => setIsRunning(true),
    stop: () => setIsRunning(false),
    reset: () => {
      setTime(initialTime);
      setIsRunning(false);
    }
  };
}

API State Machine

function useApiState(initialState = 'idle') {
  const [state, setState] = useState(initialState);
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  // Show state machine status
  useDebugValue(
    { state, hasData: !!data, hasError: !!error },
    ({ state, hasData, hasError }) => {
      const flags = [];
      if (hasData) flags.push('data');
      if (hasError) flags.push('error');
      return `${state}${flags.length ? ` [${flags.join(', ')}]` : ''}`;
    }
  );

  return {
    state,
    data,
    error,
    setState,
    setData,
    setError
  };
}

Without Formatter

function useCounter(initialCount = 0) {
  const [count, setCount] = useState(initialCount);

  // Simple value display
  useDebugValue(count);

  return [
    count,
    () => setCount(c => c + 1),
    () => setCount(c => c - 1)
  ];
}
useDebugValue only affects the display in Preact DevTools. It has no effect on production code behavior and minimal performance impact when devtools are closed.
Use the optional formatter parameter for expensive formatting operations. The formatter function will only be called when devtools are open and inspecting the component, avoiding unnecessary computation.
useDebugValue is most useful in custom hooks that are used in multiple places. For component-local state, the regular devtools inspection is usually sufficient.

Build docs developers (and LLMs) love