The react-test-renderer package renders a React component without any DOM. What does `TestRenderer.create(<Profile />).toJSON()` return, and why would a project want a renderer that produces that instead of DOM nodes?
answer
- reconciler and host layer are separate
- host instances here are plain objects
- type, props, children — no document
- your components flatten away
- born for React Native, no DOM there
basics
~20 stoJSON() returns a plain JavaScript object tree of the host elements React produced — each node has type, props and children, with text as plain strings. No DOM node is created, so the renderer runs anywhere React runs, including React Native.
solid answer
~40 s`TestRenderer.create()` runs React's normal reconciliation, but its host layer builds plain JavaScript objects instead of DOM nodes. Calling `.toJSON()` on the returned renderer serialises the *host* part of that tree: each node is roughly `{ type, props, children }`, where `type` is the host element name such as `'div'` or `'View'`, and text children are plain strings. Your own components do not appear — only what they rendered down to. The point is host-independence: React's reconciler is separate from the thing that creates host instances, so this renderer works with no `document` at all. That is why React Native adopted it, since there are no DOM nodes on a phone, and why it feels fast in a bare Node process. The same renderer also exposes `.root` for walking the tree, plus `.update()` and `.unmount()`.
code
javascript · 19 linesconst React = require('react');
const TestRenderer = require('react-test-renderer');
function Profile({ name }) {
return React.createElement(
'div',
{ className: 'profile' },
React.createElement('h1', null, name),
);
}
const renderer = TestRenderer.create(
React.createElement(Profile, { name: 'Ada' }),
);
console.log(JSON.stringify(renderer.toJSON(), null, 2));
// { "type": "div", "props": { "className": "profile" },
// "children": [ { "type": "h1", "props": {}, "children": ["Ada"] } ] }
// Note: Profile itself does not appear - only the host elements it produced.go deeper
Be able to say plainly that this renderer produces a tree of plain JavaScript objects with type, props and children, and that no DOM node is created anywhere.
Explain the mechanism: React's reconciler is host-agnostic, and this package supplies a host layer that allocates objects. Note that toJSON shows only host elements while root and toTree keep component boundaries.
Be ready to judge what such a tree can and cannot prove about a component, and to say which assertions in an inherited suite are actually load-bearing versus merely describing structure.
Own the framing that renderer choice is an environment-fidelity decision with a cost curve — object tree, simulated DOM, real browser — and be able to argue where a given codebase should sit and why.
## React has two halves, and only one of them is the DOM React is not one library. The **reconciler** decides what the UI should look like — it runs your components, diffs element trees and works out what changed. A **host renderer** turns those decisions into something real: `react-dom` creates and mutates DOM nodes, React Native creates native views, and `react-test-renderer` creates ordinary JavaScript objects. All three drive the identical reconciler, so hooks, state, effects and reconciliation behave the same in each. Only the host instances differ. That split is the whole reason a DOM-less renderer can exist. When people say "react-test-renderer renders without a DOM", they mean its host layer never calls `document.createElement`; it allocates a small object per host element instead. ## What create() hands back ```js const TestRenderer = require('react-test-renderer'); const renderer = TestRenderer.create(React.createElement(Profile, { name: 'Ada' })); ``` `renderer` is not the tree — it is a handle onto a mounted render, with a few members: - `renderer.toJSON()` — the serialised host tree (below). - `renderer.toTree()` — a richer tree that also includes your composite components. - `renderer.root` — a test instance you can walk with `findByType`, `findAllByType`, `findByProps`, reading `.props` and `.children`. - `renderer.update(element)` — re-render at the root, the equivalent of a parent passing new props. - `renderer.unmount()` — unmount and run cleanups. ## The shape of toJSON output Each node is a plain object with three fields: `type`, `props` and `children`. `type` is the host element name — `'div'`, `'input'`, `'View'` — never one of your components. `props` are the host props that were passed down. `children` is an array whose entries are either more nodes or plain strings for text, or `null` when there are none. Rendering nothing at all yields `null`. ```js // { type: 'div', // props: { className: 'profile' }, // children: [ { type: 'h1', props: {}, children: ['Ada'] } ] } ``` The important detail is that your components have already been flattened away. A `<Profile>` that renders a `<Card>` that renders a `<div>` shows up as just the `div`. That mirrors what the DOM would end up containing, which is exactly why this output has historically been fed to structural snapshot assertions. ## Why anyone wanted plain objects Three reasons, in rough order of importance. **React Native has no DOM.** On a phone the host instances are native views owned by the platform, and you cannot spin those up in a unit test. A renderer that fabricates plain objects gives React Native a way to run component logic in a plain Node process at all. This is the original motivation and the reason the package survived in the React Native world long after web testing moved on. **No environment to set up.** Rendering into a DOM means running a DOM implementation such as jsdom, or a real browser. The object renderer needs neither, so it starts fast and has fewer moving parts. **The result is inspectable data.** A tree of plain objects can be walked, serialised and diffed with ordinary JavaScript, without any query API. ## What the output is emphatically not It is not the DOM, and nothing DOM-shaped happens around it. There is no layout, no CSS, no focus, no scrolling, and no browser event system. Refs to host elements are `null` unless you supply a node mock. "Clicking" means reaching into the tree and invoking a prop function yourself — no bubbling, no default behaviour, no `disabled` guard, no capture phase. So a green result tells you the component *produced a certain shape*, not that a user could operate it. ## Where it stands today React 19 deprecates `react-test-renderer` and logs a warning when it is used; the recommended paths are `@testing-library/react` for web components and `@testing-library/react-native` for React Native. Understanding the object tree is still worth having: it explains what the older tests in a legacy suite are actually asserting, and it is a clean illustration of how far a renderer can be from the environment your users see.
- If toJSON() only shows host elements, how would you inspect a component boundary in that tree?Use `renderer.root` rather than `toJSON()`. The root test instance keeps composite components, so `renderer.root.findByType(Card)` locates the component node and lets you read its `props`. `renderer.toTree()` similarly returns a tree that includes component nodes. Both are inspecting structure rather than behaviour, though — useful for debugging, weak as an assertion target, because they bind the test to how the component is decomposed internally.
- Does state, effects and reconciliation behave differently under this renderer than under react-dom?No — the reconciler is shared. Hooks, state updates, keys, bailouts and effect ordering are the same code path; only the host layer differs. What changes is everything the host owns: element creation, event dispatch, layout, focus and refs. So component *logic* is exercised faithfully, while component *interaction* is not exercised at all.
- Why does the returned tree contain no CSS or computed styles?Because styling belongs to the host. `react-dom` hands class names and inline styles to a browser that resolves the cascade and computes layout; the object renderer just parks the `className` or `style` prop on a plain object and stops. Nothing resolves them, so any test about appearance, spacing or visibility has to run somewhere that actually has a styling engine.
It is like reviewing a building from the bill of materials rather than from the finished building: the list tells you which parts were ordered and in what arrangement, but nothing about whether the doors open.
saying these in an interview costs you the question
- Claims toJSON returns an HTML string of the markup
- Thinks it renders into jsdom under the hood
- Expects your own components to appear in the JSON tree
- Believes querySelector or getByRole works on the result
- Assumes hooks behave differently because there is no DOM