A quick React debug panel often starts here:
<pre>{JSON.stringify(data, null, 2)}</pre>
For plain JSON data, that's a reasonable place to start. Then a Map appears as {}, and the panel has quietly stopped showing the thing you were trying to debug.
Here's a small example you can run in a JavaScript console:
const user = { id: 7, name: "Ada" };
const data = {
cache: new Map([[7, user]]),
roles: new Set(["editor"]),
updatedAt: new Date("2026-10-06T08:00:00Z"),
missing: undefined,
primary: user,
selected: user,
};
console.log(JSON.stringify(data, null, 2));
The output is:
{
"cache": {},
"roles": {},
"updatedAt": "2026-10-06T08:00:00.000Z",
"primary": { "id": 7, "name": "Ada" },
"selected": { "id": 7, "name": "Ada" }
}
The Map entries and Set values are gone. The Date became a string. The missing property disappeared. And you can't tell that primary and selected point to the same object.
Those are expected JSON.stringify semantics. They just happen to remove information that can matter in a debugging interface.
Shared objects and cycles are different problems
In that example:
data.primary === data.selected; // true
There is no cycle. Two properties share one object. If a viewer renders two unrelated copies, a useful relationship gets lost.
Now add this:
data.self = data;
JSON.stringify(data); // throws TypeError
This time there is a cycle. A viewer needs to stop following it while still showing where the reference goes.
A replacer can make a value serializable, but you then have to decide how to represent collections, identity and reference targets. Those decisions become part of your debug panel.
Inspection can execute code, too
This example has another surprise:
let reads = 0;
const value = {
get status() {
reads += 1;
return "ready";
},
};
JSON.stringify(value);
console.log(reads); // 1
Reading the property ran its getter. That getter could have been expensive, stateful or capable of throwing.
For a panel that opens automatically, I want to inspect the property descriptor without calling the getter. The panel can show that an accessor exists and leave its execution alone.
The React component I built for this
These are the cases React Data Inspector is designed to handle. I maintain it as an open-source React component for inspecting JavaScript values, including their types and reference relationships.
Install it:
npm install @nipe-solutions/react-data-inspector
Then try this in a JSX file in your React app:
import { DataInspector } from "@nipe-solutions/react-data-inspector";
import "@nipe-solutions/react-data-inspector/styles.css";
const user = { id: 7, name: "Ada" };
const debugData = {
cache: new Map([[7, user]]),
roles: new Set(["editor"]),
updatedAt: new Date("2026-10-06T08:00:00Z"),
missing: undefined,
primary: user,
selected: user,
};
debugData.self = debugData;
export default function DebugPanel() {
return <DataInspector value={debugData} />;
}
The inspector distinguishes shared references from circular ones, and supports navigation to reference targets. It also has search through collapsed data, controlled expansion and selection, and tree keyboard navigation. Large values use hierarchical ranges and bounded rendering.
It is read-only: it doesn't evaluate getters, await promises or execute functions. WeakMap and WeakSet contents remain opaque. The README covers the supported types and customization options.
If you just need a formatted API response made of JSON-compatible values, the small <pre> example may already do the job. The inspector is for cases where the richer JavaScript value is what you need to see.
You can try the playground before installing it.
I'm looking for feedback from people building internal tools, SDK diagnostics or development panels. I'd especially like to hear about awkward object graphs, slow interactions with large values, and places where keyboard navigation gets in the way. A minimal reproduction in GitHub Issues would help me turn that into a fix.
What is the first object you'd put through it?
I also maintain ReadonlyView, which tackles a related state boundary: exposing a lazy, live readonly view while keeping owner-side updates visible. You can find it and my other frontend libraries at NIPE Open Source.
Top comments (0)