High-performance React component for inspecting deeply nested objects with virtualization, grouping, and deterministic value getters.
React Object View targets React 19 projects (Node 22+ / Yarn 4 recommended) and ships a TypeScript-first API with ESM + UMD bundles.
- Virtualized tree view β only visible rows render, so 100k+ nodes stay smooth.
- Async Rendering β non-blocking tree traversal keeps the UI responsive even when processing massive datasets.
- Resolver system β promises, maps, sets, errors, dates, regexes, iterables, grouped proxies, typed arrays, buffers, and custom classes.
- Sticky path headers β pin ancestor rows while scrolling so nested contexts stay visible.
- Grouping for huge payloads β
arrayGroupSize&objectGroupSizebucket massive collections (objects must be enumerated firstβsee note below). Groups are collapsed by default to ensure minimal impact on render performance. - TypeScript-native β published
.d.tsand React 19 JSX runtime support. - Zero dependencies β lightweight and self-contained (besides React).
- Styling hooks β CSS variables + theme presets plus
className/styleescape hatches. - Generic tree APIs β build custom tree views for non-object data structures (files, ASTs, etc.).
- Copy to clipboard β built-in action buttons to copy primitive values or JSON-serialized objects.
- Change awareness β optional flashing highlights updated values.
- Interactive hover β highlights the indentation guide for the current parent context.
- Line numbers β optional gutter with 0-based indices for debugging.
- Search & Navigation β streamed, debounced search with keyboard shortcuts, highlighting, and jump-to-match navigation.
npm install react-obj-view
# or
yarn add react-obj-viewimport { ObjectView } from "react-obj-view";
import "react-obj-view/dist/react-obj-view.css";
const user = {
name: "Ada",
stack: ["TypeScript", "React"],
meta: new Map([["lastLogin", new Date()]]),
};
export function DebugPanel() {
return (
<ObjectView
valueGetter={() => user}
name="user"
expandLevel={2}
/>
);
}Add the floating SearchComponent if you want Cmd/Ctrl+F to pop open a search bar with match navigation out of the box.
const valueGetter = useCallback(() => user, [user]);
<ObjectView valueGetter={valueGetter} />;Wrap dynamic data in useMemo/useCallback so the virtual tree only re-walks when the underlying value actually changes.
| Prop | Type | Default | Description |
|---|---|---|---|
valueGetter |
() => unknown |
required | Lazily supplies the data that should be rendered. |
name |
string |
undefined |
Optional root label shown before the first colon. |
expandLevel |
number | boolean |
false |
Depth of initial expansion; true expands everything (up to depth 20). |
objectGroupSize |
number |
0 |
Enable grouping for objects when they exceed this many keys. Groups are collapsed by default for performance. Objects must be fully enumerated to detect size, so only enable this when you need grouped previews and can afford the enumeration cost. |
arrayGroupSize |
number |
0 |
Splits very large arrays into range buckets ([0β¦999]) for faster navigation. Groups are collapsed by default for performance. |
resolver |
Map<any, ResolverFn> |
undefined |
Merge in custom resolvers keyed by constructor. |
highlightUpdate |
boolean |
false |
Flash updated values via useChangeFlashClasses. |
stickyPathHeaders |
boolean |
true |
Pins the current node's ancestor label while you scroll through its children; disable to revert to free-scrolling rows. |
preview |
boolean |
true |
Show inline previews (Array(5), 'abcβ¦') on collapsed rows. |
nonEnumerable |
boolean |
false |
Include non-enumerable properties during traversal. |
includeSymbols |
boolean |
false |
Include symbol keys when enumerating or previewing objects. |
showLineNumbers |
boolean |
false |
Display a gutter with zero-based line numbers. |
lineHeight |
number |
14 |
Row height (in px) used by the virtual scroller. Keep this in sync with your CSS/fonts; mismatches cause rows to drift/overlap because virtualization still uses the old size. |
style |
React.CSSProperties |
undefined |
Inline styles applied to .big-objview-root (theme presets are plain objects). |
className |
string |
undefined |
Extra class hooked onto .big-objview-root. |
ref |
RefObject<ObjectViewHandle> |
undefined |
Exposes search(filterFn, markTerm, onResult, options) (options: iterateSize, maxDepth, fullSearch, maxResult default 99999) and scrollToPaths(paths, scrollOpts) (forwards ScrollToOptions) for jump-to-match navigation, used by the floating SearchComponent. Keep the ref stable and memoize search options. |
actionRenders |
React.FC<ObjectViewRenderRowProps> |
DefaultActions |
Custom component to render row actions (copy, expand, etc.). See example. |
iterateSize |
number |
100000 |
Controls the number of steps the async walker performs before yielding to the main thread. Lower values improve responsiveness but may increase total render time. |
π Need more detail? Check the API Documentation.
The package exports several ready-made palettes:
import { ObjectView } from "react-obj-view";
import { themeMonokai } from "react-obj-view";
<ObjectView valueGetter={getter} style={themeMonokai} />;Prefer CSS? Override the variables directly:
.big-objview-root {
--bigobjview-color: #e5e9f0;
--bigobjview-bg-color: #1e1e1e;
--bigobjview-type-string-color: #c3e88d;
--bigobjview-type-number-color: #f78c6c;
}<ObjectView valueGetter={getter} className="object-view" />Note: The built-in themeDefault adapts automatically to light and dark modes using CSS light-dark() and the user's prefers-color-scheme. Other presets (e.g., One Dark, Dracula, Monokai) are static and do not change automatically. The demoβs light/dark/auto toggle affects the page chrome only β ObjectView does not auto-switch its theme; choose a preset explicitly if you want a specific look. Additional light presets (themeGitHubLight, themeSolarizedLight, themeQuietLight) plus themeGeneral ship from the root entry alongside the dark palettes.
Want full control? Build your own palette with the exported helpers:
import {
createTheme,
extendTheme,
themeDefault,
} from "react-obj-view";
// Build from scratch (every CSS variable from the table below is required)
const midnight = createTheme({
"--bigobjview-color": "#e8eaed",
"--bigobjview-bg-color": "#0b1116",
"--bigobjview-change-color": "#ff8a65",
"--bigobjview-fontsize": "12px",
"--bigobjview-type-boolean-color": "#18ffff",
"--bigobjview-type-number-color": "#ffab40",
"--bigobjview-type-bigint-color": "#ff6d00",
"--bigobjview-type-string-color": "#ffee58",
"--bigobjview-type-object-array-color": "#40c4ff",
"--bigobjview-type-object-object-color": "#7e57c2",
"--bigobjview-type-object-promise-color": "#ec407a",
"--bigobjview-type-object-map-color": "#00e5ff",
"--bigobjview-type-object-set-color": "#26c6da",
"--bigobjview-type-function-color": "#80cbc4",
"--bigobjview-type-object-regexp-color": "#ef5350",
"--bigobjview-type-object-date-color": "#8bc34a",
"--bigobjview-type-object-error-color": "#ff7043",
"--bigobjview-action-btn": "#444",
"--bigobjview-action-success": "#00e676",
"--bigobjview-action-error": "#ff5252",
});
// β¦or extend an existing preset
const midnightCondensed = extendTheme(midnight, {
"--bigobjview-fontsize": "11px",
lineHeight: 12,
});Refer to the βStyling referenceβ table in the docs whenever you need the full list of supported CSS variables.
Line-height tip: If your theme tweaks fonts or padding, expose a shared CSS variable (e.g.
--rov-row-height) and set both.row { height: var(--rov-row-height) }and thelineHeightprop from the same value so scrolling math stays correct.
class ApiEndpoint {
constructor(
public method: string,
public url: string,
public status: number,
public responseTime: number,
) {}
}
const resolver = new Map([
[
ApiEndpoint,
(endpoint, cb, next, isPreview) => {
if (isPreview) {
cb('summary', `${endpoint.method} ${endpoint.url}`, true);
cb('status', endpoint.status, true);
return;
}
cb('responseTime', `${endpoint.responseTime}ms`, true);
next(endpoint);
},
],
]);
<ObjectView valueGetter={() => data} resolver={resolver} />;<ObjectView
valueGetter={() => largeObject}
objectGroupSize={250}
arrayGroupSize={500}
/>- Arrays get chunked up immediately because their length is known.
- Objects must be enumerated to count keys. Use grouping when the trade-off (initial enumeration vs. quicker navigation) makes sense for the payload.
Each row includes built-in action buttons:
// Primitives get a "Copy" button
const config = { apiKey: "sk-abc123", timeout: 5000 };
<ObjectView valueGetter={() => config} />
// Hover over any row to see Copy / Copy JSON buttons
// Copy actions show success/error feedback
// Automatically resets after 5 seconds- Copy button for strings, numbers, bigints β copies the raw value
- Copy JSON button for objects, arrays, dates β serializes via
JSON.stringify()
The viewer highlights the indentation guide for the current parent context on hover, making it easier to trace parent-child relationships:
<ObjectView valueGetter={() => deeplyNested} expandLevel={3} />
// Hover over any row to see visual feedback
// CSS custom properties (--active-index, --active-parent) enable theme customizationNo configuration neededβthe feature is built-in and adapts to your theme.
Enable a gutter with 0-based line numbers for easier debugging:
<ObjectView
valueGetter={() => largeData}
showLineNumbers={true}
lineHeight={18}
/>ObjectView exposes a streaming search handle via ref. The built-in floating SearchComponent wires it up and renders a loading indicator while results stream in:
import { ObjectViewHandle, SearchComponent } from "react-obj-view";
const objViewRef = useRef<ObjectViewHandle | null>(null);
const [searchActive, setSearchActive] = useState(false);
<ObjectView valueGetter={() => data} ref={objViewRef} />
<SearchComponent
active={searchActive}
onClose={() => setSearchActive(false)}
// SearchComponent builds the filter fn + highlight regex for you
handleSearch={(filterFn, markTerm, onResult, opts) =>
objViewRef.current?.search(filterFn, markTerm, onResult, {
...opts,
maxResult: 5000,
})
}
scrollToPaths={(paths, scrollOpts) => objViewRef.current?.scrollToPaths(paths, scrollOpts)}
// Optional: normalize diacritics or tweak search iteration depth
options={{
normalizeSymbol: (c) => c.normalize("NFD").replace(/\p{M}/gu, ""),
maxDepth: 8,
}}
/>;- Keyboard shortcuts: press Enter to jump to the next match (Shift+Enter for previous), and Escape to clear/close.
- Results stream via
requestIdleCallbackand update highlights with themarkTermregex. Stop typing to debounce;maxResult,maxDepth,fullSearch, anditerateSizecontrol scope and responsiveness.
Search options supported by ObjectViewHandle.search and the built-in SearchComponent:
iterateSize: override traversal batch size when searching.maxDepth: cap how deep the walker searches.fullSearch: force traversal of already-collapsed branches.maxResult: limit streamed matches (default99999).normalizeSymbol: SearchComponent-only hook to normalize characters (e.g., strip diacritics) before matching.
scrollToPaths accepts the same options as ScrollToOptions when jumping to highlighted matches.
The library now exports generic tree APIs for non-object data:
import { walkingFactory, type WalkingAdapter } from 'react-obj-view';
// Define your domain (e.g., file system, AST, org chart)
type FileNode = {
name: string;
type: 'folder' | 'file';
children?: FileNode[];
};
// Implement the adapter
const fileAdapter: WalkingAdapter<...> = {
valueHasChild: (node) => node.type === 'folder' && !!node.children?.length,
iterateChilds: (node, ctx, ref, cb) => {
node.children?.forEach(child => cb(child, child.name, { ... }));
},
// ... other methods
};
const fileTreeFactory = () => walkingFactory(fileAdapter);See Generic Tree Stack for a complete walkthrough with React integration.
- Always pass the correct
lineHeight(or follow the CSS-variable approach) when changing typography. - The component sets its container height to
lineHeight * size. If you clamp the container via CSS, ensure the scroll parent can actually scroll; otherwise virtualization canβt measure the viewport.
The repository ships a large Vitest suite (utilities, walkers, resolvers, components, integration scenarios).
npm test # run everything once
npm run test:watch # watch mode
npm run test:ui # launch Vitest UI
npm run test:coverageSee TESTING.md for coverage numbers, structure, and tips.
- Debug panels β Inspect Redux/Context refs without spamming console logs.
- API/LLM explorers β Visualize nested JSON or streaming responses with circular references.
- State machines & devtools β Pair with hot reloaders or feature flags to watch state change in real time.
- Data-heavy dashboards β Embed next to chart/table widgets so analysts can drill into raw payloads.
| Library | Scenario | Mean time* | Command |
|---|---|---|---|
| react-obj-view | Flatten ~100k-node payload (see bench/perf.bench.ts) |
23.7β―ms (42.3 ops/s) | npx vitest bench bench/perf.bench.ts |
| react-obj-view | Flatten ~1M-node payload | 253β―ms (4.0 ops/s) | npx vitest bench bench/perf.bench.ts |
| react-obj-view | Flatten ~2M-node payload | 525β―ms (1.9 ops/s) | npx vitest bench bench/perf.bench.ts |
*Measured on macOS (Apple M3 Max, Node 22.11, Vitest 4.0.8). Each sample instantiates a fresh walkingToIndexFactory, generates 10k/100k/200k user records (~100k/~1M/~2M nodes total), and walks the tree. Adjust bench/perf.bench.ts to mirror your datasets if you need environment-specific numbers.
Third-party libraries arenβt benchmarked here; run their official examples under the same conditions for apples-to-apples comparisons.
- Usage Guide β end-to-end patterns, resolver recipes, styling guidance.
- API Documentation β deeper dive into props, hooks, and resolver authoring.
- Generic Tree Stack β explains
tree-core,react-tree-view, and the virtual-scroller for building custom viewers. - Object Tree Adapter & React Viewer β details how the built-in
ObjectViewcomposes the generic stack with resolvers. - Live demo β try grouping, previews, and change flashes in the browser.
git clone https://github.com/vothanhdat/react-obj-view
cd react-obj-view
yarn install
yarn devMIT Β© Vo Thanh Dat
