useDebugValue
Label a custom Hook's value in React DevTools so its internals are readable while debugging.
useDebugValue(value, format?)- Added in React 16.8.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$value | any | The value you want to display in React DevTools. It can have any type. |
$formatoptional | (value: any) => any | A formatting function. When the component is inspected, React DevTools will call the formatting function with the value as the argument, and then display the returned formatted value (which may have any type). If you don't specify the formatting function, the original value itself will be displayed. |
Return value
voiduseDebugValue does not return anything.
Errors
Nothing, but as a Hook it triggers React's invalid-hook-call error if called outside the top level of a component or custom Hook.
Changelog
- PHP 16.8.0Introduced alongside Hooks so custom Hooks can expose a readable label to React DevTools.
useDebugValue attaches a label to a custom Hook so React DevTools shows something meaningful next to it. Without it, DevTools lists a custom Hook by name with its internal useState and useRef entries nested underneath, which is unreadable once the Hook holds anything structured.
When to reach for it
Reach for it in custom Hooks that other people consume — a shared package, a design system, an internal library — and whose internal shape does not read well. useOnlineStatus showing true is less useful than "Online"; a subscription Hook showing three refs is less useful than "connected (3 listeners)".
Do not add it to every Hook you write. For a Hook that wraps a single boolean or string, DevTools already shows the value clearly and the call is noise.
What trips people up
- It renders nothing. It returns
undefinedand produces no DOM. If you are looking for output on screen, you want ordinary state. - Component-level calls do nothing useful. The label is meant for custom Hooks; calling it directly inside a component is not the intended use.
- Pass
formatas a function, do not call it. WriteuseDebugValue(date, d => d.toISOString()), notuseDebugValue(date.toISOString()). The second form runs the expensive work on every render; the first runs only when someone actually opens the inspector. - You need the DevTools extension. With no DevTools attached, the call is inert.
Example
import { useDebugValue, useEffect, useState } from "react";
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const goOnline = () => setIsOnline(true);
const goOffline = () => setIsOnline(false);
window.addEventListener("online", goOnline);
window.addEventListener("offline", goOffline);
return () => {
window.removeEventListener("online", goOnline);
window.removeEventListener("offline", goOffline);
};
}, []);
// React DevTools shows: OnlineStatus: "Online"
useDebugValue(isOnline ? "Online" : "Offline");
return isOnline;
}
export default function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? "Online" : "Disconnected"}</h1>;
}When to use it
- Show "Online" / "Offline" beside a useOnlineStatus Hook instead of a bare boolean.
- Label a data-fetching Hook with its current status so DevTools reads "loading" or "error: 404".
- Format a Date or a large object lazily, so the cost is paid only when a developer inspects the component.
- Expose the active key of a subscription Hook shared across a component library.
Frequently asked questions
Why does useDebugValue not show anything?
Because it only writes to the React DevTools Components panel, and only for custom Hooks. Install the DevTools browser extension, select the component that calls your Hook, and look at the Hooks list on the right. It renders nothing to the page and returns undefined, so there is nothing to see in the app itself.
Does useDebugValue slow down production builds?
It is a no-op when no DevTools is attached, and passing the optional `format` function means any expensive conversion runs only on inspection. The one way to make it costly is to compute the label eagerly, as in `useDebugValue(bigObject.toString())` — pass the value and a formatter instead.
Should I add useDebugValue to every custom Hook?
No. React's docs recommend it for Hooks that are part of shared libraries and have a complex internal data structure that is hard to inspect. For a Hook wrapping a single boolean or string, DevTools already displays the value clearly and the extra call is noise.
More examples
Deferring an expensive format
DevTools shows `LastSync: "2024-01-01T09:30:00.000Z"` beside the Hook, but `toISOString` is only invoked when a developer selects the component in the Components panel. That is the whole point of the second argument: formatting work that would be wasteful on every render becomes free until someone looks.
import { useDebugValue, useState } from "react";
function useLastSync() {
// Pinned so the label is the same on every inspection.
const [syncedAt] = useState(() => new Date("2024-01-01T09:30:00Z"));
// The arrow function runs ONLY when the component is inspected.
useDebugValue(syncedAt, (date) => date.toISOString());
// Not this - toISOString() would run on every single render:
// useDebugValue(syncedAt.toISOString());
return syncedAt;
}
export default function SyncBadge() {
const syncedAt = useLastSync();
return <p>Last sync: {syncedAt.toISOString()}</p>;
}Labelling a request status Hook
Without the Hook label, DevTools shows a nested `useState` entry holding `{status: "success", data: {…}}` that you have to expand. With it, the Hooks panel reads `Resource: "success: Ada Lovelace"` on one line. Note that DevTools strips the leading `use` from the Hook name when it builds the label.
import { useDebugValue, useEffect, useState } from "react";
function useResource(key) {
const [state, setState] = useState({ status: "loading", data: null });
useEffect(() => {
let cancelled = false;
// A resolved promise stands in for a real request here.
Promise.resolve({ id: key, name: "Ada Lovelace" }).then((data) => {
if (!cancelled) setState({ status: "success", data });
});
return () => {
cancelled = true;
};
}, [key]);
useDebugValue(state, (s) =>
s.status === "success" ? `success: ${s.data.name}` : s.status
);
return state;
}
export default function ProfileCard() {
const { status, data } = useResource(42);
if (status === "loading") return <p>Loading...</p>;
return <p>Name: {data.name}</p>;
}Where it belongs, and where it does not
Inspecting `Toolbar` shows `Selection: "2 of 3 selected"` for the first Hook, because a `Set` renders as an opaque entry that is tedious to expand. The second Hook is deliberately left unlabelled: a boolean already reads fine in the panel, and adding a debug value there only adds a line to scroll past.
import { useDebugValue, useState } from "react";
// Worth labelling: a shared Hook with a structured internal value.
function useSelection(items) {
const [selectedIds, setSelectedIds] = useState(() => new Set());
useDebugValue(selectedIds, (set) =>
set.size === 0 ? "none" : `${set.size} of ${items.length} selected`
);
return [selectedIds, setSelectedIds];
}
// Not worth labelling: DevTools already shows this clearly.
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
// useDebugValue(on); <- redundant noise
return [on, () => setOn((v) => !v)];
}
export default function Toolbar() {
const rows = ["alpha", "beta", "gamma"];
const [selected, setSelected] = useSelection(rows);
const [expanded, toggle] = useToggle();
return (
<div>
<button onClick={toggle}>{expanded ? "Collapse" : "Expand"}</button>
{expanded && (
<ul>
{rows.map((row, i) => (
<li key={row}>
<label>
<input
type="checkbox"
checked={selected.has(i)}
onChange={() =>
setSelected((prev) => {
const next = new Set(prev);
next.has(i) ? next.delete(i) : next.add(i);
return next;
})
}
/>{" "}
{row}
</label>
</li>
))}
</ul>
)}
<p>{selected.size} selected</p>
</div>
);
}
Discussion