React 49 ⚛️ Custom Hook Debug Labels with useDebugValue
A custom hook is a black box. The component calls useOnlineStatus() or useFetch(url) or useFormInput(initial), and the hook does its work behind the abstraction. When the hook works, the abstraction is a blessing: the component is clean, and the logic is in one place. When the hook misbehaves, the abstraction is a curse: the React DevTools show the hook’s internal state as a series of the anonymous State and Effect entries, and the developer must read the source to understand what is happening. useDebugValue is the hook that labels the custom hook in the DevTools. It attaches a value to the hook, and the DevTools display the value next to the hook’s name. The label is the developer-experience improvement, and it costs nothing in the production.
The hook is deliberately narrow. It takes the value and the optional format function. The value is the label the DevTools display, and the format function transforms the value before the display. The format function is called only when the DevTools are open, so the expensive formatting does not run in the application. The hook is the debug-only tool, and the React team’s guidance is to use it only in the custom hooks that are the part of the shared libraries, not in the everyday hooks. The label is the documentation for the developer who inspects the DevTools.
The hook has the subtle behaviors. It is called at the top level of the custom hook, not in the component. It can be called conditionally, unlike the other hooks, because the label is only for the display. It does not affect the rendering, and it does not cause the re-renders. The value can be the object, the array, or the primitive, and the format function can turn the object into the readable string. The DevTools show the formatted value, and the developer sees the hook’s state without reading the source.
This chapter covers three areas. First, why useDebugValue exists — the DevTools opacity and the debugging experience. Second, how to use it — the signature, the format function, the conditional call, and the interaction with the custom hooks. Third, the patterns and the pitfalls — the label patterns, the format functions, the production behavior, and the cases where the hook is unnecessary. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the DevTools display.
Key point: useDebugValue labels a custom hook in the React DevTools. It takes the value and the optional format function, and the DevTools display the formatted value next to the hook. The format function runs only when the DevTools are open, so the formatting cost is the debug-only. The hook does not affect the rendering or the re-renders, and it can be called conditionally. Use it in the shared custom hooks, not in the everyday hooks.
Why useDebugValue exists
The DevTools opacity problem. The React DevTools show the component tree, the props, the state, and the hooks. The built-in hooks — the useState, the useEffect, the useRef — have the recognizable names, and their values are visible. The custom hooks appear as the sequence of the anonymous entries: the State, the State, the Effect, the Memo, and the developer cannot tell which entry belongs to which custom hook. The useDebugValue attaches the label to the custom hook, and the DevTools display the label next to the hook’s name. The black box becomes the labeled box.
The library-consumer problem. A shared library exposes the custom hooks — the useOnlineStatus, the useTheme, the useForm — and the consumers use them without reading the source. When the consumer debugs, the DevTools show the anonymous entries, and the consumer cannot tell what the hook is doing. The useDebugValue gives the library author the way to label the hook, and the consumer sees the meaningful value. The library author provides the label, and the consumer benefits.
The format-function problem. The value the hook wants to display may not be the value the DevTools can render. The value may be the object with the many fields, the nested structure, or the complex computation. The format function transforms the value into the readable string. The function runs only when the DevTools are open, so the expensive formatting — the JSON serialization, the deep inspection — does not run in the application.
The production-cost problem. The debugging tools should not cost the production. The useDebugValue is the no-op in the production build, and the value and the format function are the dead code that the bundler can eliminate. The hook is the debug-only, and the cost is the zero in the production.
The conditional-call problem. The useDebugValue is the exception to the Rules of Hooks. It can be called conditionally, because the label is only for the display, and the call order does not matter. The other hooks must be called unconditionally, but the useDebugValue can be inside the if, and the DevTools show the label only when the condition is true.
The trade-off. The useDebugValue is the code that exists only for the debugging. The hook adds the line to the custom hook, and the line is the noise for the reader who does not use the DevTools. The React team’s guidance is to use it only in the shared libraries, where the many consumers benefit, not in the everyday hooks, where the label is the noise. The trade-off is between the debugging benefit and the code clarity.
a. The basic useDebugValue
The useDebugValue takes the value and the optional format function. It is called at the top level of the custom hook.
import { useState, useEffect, useDebugValue } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
const unsubscribe = subscribeToStatus(handleStatusChange);
return unsubscribe;
}, []);
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
The useDebugValue attaches the 'Online' or the 'Offline' to the useOnlineStatus hook. When the developer inspects the component in the DevTools, the hook shows the label instead of the anonymous State entry. The label is the value the hook wants the developer to see.
The call location. The useDebugValue is called inside the custom hook, not in the component that uses the hook. The label is attached to the custom hook, and the DevTools display it next to the hook’s name.
// ✅ The call inside the custom hook
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
// ❌ The call in the component
function Component() {
const isOnline = useOnlineStatus();
useDebugValue(isOnline ? 'Online' : 'Offline'); // the wrong place
return <div>{isOnline ? 'Online' : 'Offline'}</div>;
}
The call in the component attaches the label to the component, not to the custom hook, and the label is the wrong.
The value type. The value can be the primitive, the object, or the array. The DevTools render the primitive directly, and the object and the array with the default inspection.
useDebugValue(isOnline); // the boolean
useDebugValue({ isOnline, lastChanged }); // the object
useDebugValue([isOnline, count]); // the array
b. The format function
The format function transforms the value before the DevTools display it. It receives the value and returns the formatted value. The function runs only when the DevTools are open.
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useDebugValue(isOnline ? 'Online' : 'Offline', (status) => {
return `Status: ${status}`;
});
return isOnline;
}
The format function receives the 'Online' or the 'Offline' and returns the 'Status: Online' or the 'Status: Offline'. The DevTools display the formatted string.
The expensive formatting. The format function is the place for the expensive formatting, because it runs only when the DevTools are open. The JSON.stringify, the deep inspection, and the complex computation are the candidates.
function useFormState(initial) {
const [state, setState] = useState(initial);
useDebugValue(state, (s) => {
return JSON.stringify(s, null, 2);
});
return [state, setState];
}
The JSON.stringify runs only when the DevTools are open, and the application does not pay the cost.
The Date formatting. The common pattern is the Date formatting:
function useLastUpdated() {
const [date, setDate] = useState(new Date());
useDebugValue(date, (d) => d.toISOString());
return [date, setLastUpdated];
}
The toISOString runs only when the DevTools are open, and the display is the readable ISO string.
The custom format. The format function can return the object with the nested structure, and the DevTools render it as the expandable tree:
useDebugValue(state, (s) => ({
count: s.count,
items: s.items.length,
status: s.status,
}));
The DevTools show the three fields, and the developer sees the summary without the full state.
c. The patterns
The label pattern. The simplest pattern is the label for the state:
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
useDebugValue(value ? 'On' : 'Off');
return [value, setValue];
}
The DevTools show the On or the Off next to the useToggle hook.
The summary pattern. The custom hook that manages the complex state displays the summary:
function useForm(initial) {
const [values, setValues] = useState(initial);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
useDebugValue({ values, errors, touched }, ({ values, errors, touched }) => {
const errorCount = Object.keys(errors).length;
const touchedCount = Object.keys(touched).length;
return `touched: ${touchedCount}, errors: ${errorCount}`;
});
return { values, errors, touched, setValues, setErrors, setTouched };
}
The DevTools show the touched: 2, errors: 1 summary, and the developer sees the form’s health at a glance.
The conditional pattern. The useDebugValue can be called conditionally, because the label is only for the display:
function useFetch(url, options) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useDebugValue(url, (url) => `Fetching: ${url}`);
if (options?.debug) {
useDebugValue(data, (d) => `Data: ${JSON.stringify(d).slice(0, 50)}...`);
}
return { data, loading };
}
The second useDebugValue runs only when the debug option is set, and the DevTools show the data’s preview only in the debug mode. The conditional call is the exception to the Rules of Hooks, and it is the pattern for the opt-in debug labels.
The shared-library pattern. The library authors use the useDebugValue in the public hooks, and the consumers see the labels:
// The library
export function useTheme() {
const theme = useContext(ThemeContext);
useDebugValue(theme, (t) => `Theme: ${t}`);
return theme;
}
// The consumer's component
function Button() {
const theme = useTheme();
return <button className={theme}>Click</button>;
}
The DevTools show the Theme: dark next to the useTheme hook, and the consumer sees the theme without the source.
The everyday-hook pattern. The React team’s guidance is to avoid the useDebugValue in the everyday hooks, because the label is the noise for the reader who does not use the DevTools. The hook is for the shared libraries and the hooks with the complex state.
Complete Example Session
// ============================================
// PART 1: THE BASIC useDebugValue
// ============================================
import { useState, useEffect, useDebugValue } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
const unsubscribe = subscribeToStatus(handleStatusChange);
return unsubscribe;
}, []);
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
// ============================================
// PART 2: THE FORMAT FUNCTION
// ============================================
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useDebugValue(isOnline ? 'Online' : 'Offline', (status) => {
return `Status: ${status}`;
});
return isOnline;
}
// ============================================
// PART 3: THE DATE FORMATTING
// ============================================
function useLastUpdated() {
const [date, setDate] = useState(new Date());
useDebugValue(date, (d) => d.toISOString());
const update = () => setDate(new Date());
return [date, update];
}
// ============================================
// PART 4: THE OBJECT SUMMARY
// ============================================
function useForm(initial) {
const [values, setValues] = useState(initial);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
useDebugValue({ values, errors, touched }, ({ values, errors, touched }) => {
const errorCount = Object.keys(errors).length;
const touchedCount = Object.keys(touched).length;
return `touched: ${touchedCount}, errors: ${errorCount}`;
});
return { values, errors, touched, setValues, setErrors, setTouched };
}
// The DevTools show: "touched: 2, errors: 1"
// ============================================
// PART 5: THE CONDITIONAL CALL
// ============================================
function useFetch(url, options) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useDebugValue(url, (url) => `Fetching: ${url}`);
if (options?.debug) {
useDebugValue(data, (d) => {
const preview = JSON.stringify(d).slice(0, 50);
return `Data: ${preview}...`;
});
}
useEffect(() => {
// The fetch logic.
}, [url]);
return { data, loading, error };
}
// The second useDebugValue runs only when the debug option is set.
// ============================================
// PART 6: THE SHARED LIBRARY HOOK
// ============================================
// The library
export function useTheme() {
const theme = useContext(ThemeContext);
useDebugValue(theme, (t) => `Theme: ${t}`);
return theme;
}
// The consumer
function Button() {
const theme = useTheme();
return <button className={theme}>Click</button>;
}
// The DevTools show "Theme: dark" next to the useTheme hook.
// ============================================
// PART 7: THE TOGGLE HOOK
// ============================================
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
useDebugValue(value ? 'On' : 'Off');
const toggle = () => setValue((v) => !v);
return [value, toggle];
}
// The DevTools show "On" or "Off".
// ============================================
// PART 8: THE PRODUCTION BEHAVIOR
// ============================================
// In the production build:
// - The useDebugValue is a no-op.
// - The value is not computed.
// - The format function is not called.
// - The bundler can eliminate the dead code.
// The cost in the production is zero.
// ============================================
// PART 9: THE ANTI-PATTERN
// ============================================
// ❌ Don't use the useDebugValue in the everyday hooks
function useCounter() {
const [count, setCount] = useState(0);
useDebugValue(count); // the noise for the reader
return [count, setCount];
}
// ✅ Use it in the shared library hooks
export function usePagination(options) {
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
useDebugValue({ page, total }, ({ page, total }) => {
return `Page ${page} of ${Math.ceil(total / options.pageSize)}`;
});
return { page, total, setPage, setTotal };
}
// ============================================
// PART 10: THE COMPLETE EXAMPLE
// ============================================
function useApi(endpoint, options = {}) {
const [state, setState] = useState({
data: null,
loading: true,
error: null,
});
useDebugValue(state, ({ data, loading, error }) => {
if (error) return `Error: ${error.message}`;
if (loading) return 'Loading...';
return `Data: ${data ? 'loaded' : 'empty'}`;
});
if (options.debug) {
useDebugValue(endpoint, (e) => `Endpoint: ${e}`);
}
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
const response = await fetch(endpoint);
const data = await response.json();
if (!cancelled) setState({ data, loading: false, error: null });
} catch (error) {
if (!cancelled) setState({ data: null, loading: false, error });
}
}
fetchData();
return () => {
cancelled = true;
};
}, [endpoint]);
return state;
}
// The DevTools show:
// useApi → "Loading..." or "Data: loaded" or "Error: ..."
// (in the debug mode) useApi → "Endpoint: /api/users"
function UserProfile({ userId }) {
const { data, loading, error } = useApi(`/api/users/${userId}`);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{data.name}</div>;
}
The ten parts show the basic useDebugValue, the format function, the Date formatting, the object summary, the conditional call, the shared library hook, the toggle hook, the production behavior, the anti-pattern, and the complete example.
Quick Reference
The useDebugValue Signature
| Argument | Meaning |
|---|---|
value | The label the DevTools display |
format | The optional format function |
The Call Location
| The location | The result |
|---|---|
| The custom hook | The label attached to the hook |
| The component | The label attached to the component |
| The condition | The label only when the condition is true |
The Format Function
| The purpose | The example |
|---|---|
| The Date | (d) => d.toISOString() |
| The JSON | (o) => JSON.stringify(o, null, 2) |
| The summary | ({ errors }) => \errors: ${errors.length}“ |
| The object | (o) => ({ count: o.count, status: o.status }) |
The Patterns
| Pattern | The example |
|---|---|
| The label | useDebugValue(isOnline ? 'Online' : 'Offline') |
| The summary | useDebugValue(state, (s) => \errors: ${s.errors.length}`)` |
| The conditional | if (debug) useDebugValue(...) |
| The shared library | The public hooks |
The Production Behavior
| Aspect | The behavior |
|---|---|
| The hook | The no-op |
| The value | The not computed |
| The format function | The not called |
| The cost | The zero |
Best Practices
✅ Do This:
// Use the useDebugValue in the custom hooks
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useDebugValue(isOnline ? 'Online' : 'Offline'); // ✅
return isOnline;
}
// Use the format function for the expensive formatting
useDebugValue(date, (d) => d.toISOString()); // ✅
// Use the summary for the complex state
useDebugValue(state, ({ errors }) => `errors: ${errors.length}`); // ✅
// Use the conditional for the opt-in debug
if (options.debug) useDebugValue(data, (d) => `Data: ${d}`); // ✅
// Use it in the shared library hooks
export function useTheme() {
useDebugValue(theme, (t) => `Theme: ${t}`); // ✅
return theme;
}
❌ Don’t Do This:
// Don't use the useDebugValue in the everyday hooks
function useCounter() {
useDebugValue(count); // the noise // ❌
}
// Don't call it in the component
function Component() {
useDebugValue(value); // the wrong place // ❌
}
// Don't use it for the side effects
useDebugValue(() => {
log('debug'); // the side effect // ❌
});
// Don't compute the expensive value outside the format function
useDebugValue(JSON.stringify(state)); // the cost on every render // ❌
// Don't rely on the useDebugValue for the logic
useDebugValue(value);
if (value) { ... } // the logic should not depend on the debug // ❌
// Don't forget it is a Hook
// The call must follow the rules (except the conditional) // ✅
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The label is not shown | The DevTools are not open | Open the React DevTools |
| The label is on the wrong hook | The call in the component | Call it inside the custom hook |
| The expensive formatting | The value computed outside the format | Use the format function |
| The label is the noise | The everyday hook | Use it in the shared hooks |
| The conditional call is confusing | The Rules of Hooks | The useDebugValue is the exception |
| The production cost | The computed value | The hook is the no-op; the bundler eliminates |
| The stale label | The missing dependency | The label reflects the current value |
Real-World Examples
1. The Online Status
useDebugValue(isOnline ? 'Online' : 'Offline');
2. The Date
useDebugValue(date, (d) => d.toISOString());
3. The Form Summary
useDebugValue({ values, errors }, ({ errors }) => `errors: ${errors.length}`);
4. The Conditional
if (debug) useDebugValue(data);
5. The Shared Theme
useDebugValue(theme, (t) => `Theme: ${t}`);
6. The Toggle
useDebugValue(value ? 'On' : 'Off');
7. The Pagination
useDebugValue({ page, total }, ({ page, total }) => `Page ${page} of ${total}`);
8. The API State
useDebugValue(state, ({ loading, error }) => {
if (error) return `Error: ${error.message}`;
if (loading) return 'Loading...';
return 'Loaded';
});
9. The Collection
useDebugValue(items, (items) => `${items.length} items`);
10. The Complete
function useApi(endpoint) {
const [state, setState] = useState({ data: null, loading: true });
useDebugValue(state, ({ loading }) => loading ? 'Loading...' : 'Loaded');
return state;
}
Visual
The DevTools Display
┌──────────────────────────────────────────────────────────────┐
│ THE DEVTOOLS DISPLAY │
│ │
│ WITHOUT the useDebugValue: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Component │ │
│ │ Hooks: │ │
│ │ State true │ │
│ │ Effect (the subscribe) │ │
│ │ │ │
│ │ The developer cannot tell which hook is which. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ WITH the useDebugValue: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Component │ │
│ │ Hooks: │ │
│ │ useOnlineStatus "Online" │ │
│ │ State true │ │
│ │ Effect (the subscribe) │ │
│ │ │ │
│ │ The hook is labeled with the meaningful value. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Format Function
┌──────────────────────────────────────────────────────────────┐
│ THE FORMAT FUNCTION │
│ │
│ useDebugValue(date, (d) => d.toISOString()); │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The value: the Date object │ │
│ │ │ │
│ │ The format function: (d) => d.toISOString() │ │
│ │ │ │
│ │ The DevTools display: "2024-01-15T10:30:00.000Z" │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The format function runs ONLY when the DevTools │ │
│ │ are open. In the application, the cost is the zero. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Conditional Call
┌──────────────────────────────────────────────────────────────┐
│ THE CONDITIONAL CALL │
│ │
│ function useFetch(url, options) { │
│ const [data, setData] = useState(null); │
│ │
│ useDebugValue(url, (url) => `Fetching: ${url}`); │
│ │
│ if (options?.debug) { │
│ useDebugValue(data, (d) => `Data: ${d}`); │
│ } │
│ │
│ return { data }; │
│ } │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The first label is always shown. │ │
│ │ The second label is shown only when the debug option│ │
│ │ is set. │ │
│ │ │ │
│ │ The useDebugValue can be called conditionally, │ │
│ │ unlike the other hooks. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Production Behavior
┌──────────────────────────────────────────────────────────────┐
│ THE PRODUCTION BEHAVIOR │
│ │
│ THE DEVELOPMENT: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ useDebugValue(date, (d) => d.toISOString()); │ │
│ │ → the value is passed to the DevTools │ │
│ │ → the format function is called on the display │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ THE PRODUCTION: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ useDebugValue(date, (d) => d.toISOString()); │ │
│ │ → the no-op │ │
│ │ → the value is not passed │ │
│ │ → the format function is not called │ │
│ │ → the bundler can eliminate the dead code │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The cost in the production is zero. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
useDebugValue | Labels a custom hook in the DevTools |
| The value | The label the DevTools display |
| The format | The optional function for the formatting |
| The call location | The top level of the custom hook |
| The conditional | Allowed; the exception to the Rules |
| The production | The no-op |
| The format cost | The zero in the application |
| The use case | The shared library hooks |
| The anti-pattern | The everyday hooks |
Key takeaways:
useDebugValuelabels a custom hook in the React DevTools. It attaches the value to the hook, and the DevTools display the value next to the hook’s name. The anonymousStateandEffectentries become the meaningful labels .- The call belongs inside the custom hook, not in the component. The label is attached to the hook that calls it. The call in the component attaches the label to the component, which is the wrong place .
- The format function transforms the value before the display. It receives the value and returns the formatted value. The function runs only when the DevTools are open, so the expensive formatting — the
JSON.stringify, thetoISOString— does not run in the application . - The
useDebugValuecan be called conditionally. It is the exception to the Rules of Hooks, because the label is only for the display. The conditional call is the pattern for the opt-in debug labels . - The hook is the no-op in the production. The value is not computed, the format function is not called, and the bundler can eliminate the dead code. The cost in the production is zero .
- Use it in the shared library hooks. The consumers of the library see the labels, and the debugging benefit is the many-developer. The everyday hooks are the noise for the reader who does not use the DevTools .
- The summary pattern is the common use. The custom hook that manages the complex state displays the summary — the
touched: 2, errors: 1— and the developer sees the hook’s health at a glance . - The hook does not affect the rendering. It does not cause the re-renders, and it does not change the component’s output. It is the debug-only tool, and the application is unaffected.
Remember: useDebugValue is the debug-only hook that labels a custom hook in the React DevTools. It takes the value and the optional format function, it is called inside the custom hook, and it can be called conditionally. The format function runs only when the DevTools are open, and the production cost is zero. Use it in the shared library hooks, where the many consumers benefit, not in the everyday hooks, where the label is the noise. The result is the black box that becomes the labeled box, which is the debugging experience improvement.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!