A React 19 web app's test suite still renders components with the react-test-renderer package, and CI now prints a deprecation warning for it. Why was that renderer deprecated for web testing, and what do you move those tests to?
answer
- renders a host nobody ships to
- only handles are type and props
- deprecated, warns, still runs
- user-perceivable queries replace tree walking
- assertions are the migration, not the render call
basics
~20 sreact-test-renderer renders to fake host objects rather than to a DOM, so web tests never exercise the environment users get. React 19 deprecates it and logs a warning; move web tests to @testing-library/react and React Native tests to @testing-library/react-native.
solid answer
~40 sThe renderer builds a tree of plain JavaScript objects instead of DOM nodes, so on the web it tests a environment that does not exist in production: no real event dispatch, no focus, no layout, no accessibility tree. It also pushes you toward implementation-shaped assertions, because the only way to reach anything is by component type or prop — `renderer.root.findByType(Button).props.onClick()` rather than a user action. React 19 marks the package deprecated and warns on use. For web I move to `@testing-library/react`, which renders into a DOM and queries the way a user perceives the UI; for React Native the equivalent is `@testing-library/react-native`. The migration is not mechanical: structural assertions over the object tree usually have to be rewritten as assertions about what is on screen after an interaction.
go deeper
Know that this renderer is deprecated in React 19, that it renders without a DOM, and that the modern default for React component tests is Testing Library rendering into a document.
Explain the mechanism behind the deprecation: a fake host layer means no real events, focus or accessibility tree, and the only available handles are component types and prop names, which are internals.
Be ready to plan the migration on a real suite — which files first, which assertions have no successor, and how to read newly-failing tests as bugs the fake host was hiding.
Own the policy: how the team prevents new tests landing on a deprecated path, how the burn-down is tracked and budgeted, and what fidelity level each class of component test should target long-term.
## What was deprecated, and what the warning means `react-test-renderer` is the package that renders a React tree into plain JavaScript objects rather than into a DOM. As of React 19 it is deprecated: it still works, and it logs a warning when used. Deprecation here is a direction signal, not a broken build — but it means new work should not start there, and an existing suite has a migration ahead of it. ## Why a DOM-less renderer stopped making sense on the web **It tests an environment nobody ships.** The renderer's host layer fabricates objects. Your web component, in production, lives in a document: events propagate through capture and bubble phases, `disabled` suppresses activation, focus moves, labels associate with controls, CSS decides what is visible, and the browser builds an accessibility tree. None of that exists in an object tree. A component can be completely broken in all of those dimensions and still produce the exact object shape the test asserted. **It makes implementation-shaped tests the path of least resistance.** With no DOM, there is nothing user-perceivable to find things by. The available handles are `findByType(SomeComponent)` and `findByProps({...})` — that is, the component decomposition and the internal prop names. Then the interaction is a direct call to a prop function. Both halves are internals: rename a prop, split a component in two, move a handler one level up, and the test breaks even though the product did not change. Meanwhile the test still passes if the handler is wired to a button the user cannot reach. **A capable DOM alternative exists.** Running components against a simulated DOM (jsdom) or a real browser stopped being exotic. Once the surrounding tooling made that cheap, keeping a second, lower-fidelity rendering path alive cost more than it returned. ## What replaces it - **Web components** → `@testing-library/react`. It renders into a document, exposes `screen` and queries such as `getByRole` and `findByText` that address the UI the way a user perceives it, and drives interaction through `userEvent`, which produces real event sequences rather than a bare handler call. - **React Native components** → `@testing-library/react-native`. Same philosophy, bindings for the React Native host. - **Genuinely browser-dependent behaviour** — layout, real focus management, scroll, cross-browser rendering — belongs in a test that runs in an actual browser, whichever runner the team already uses. ## What migration actually costs The mechanical part is small: swap the render call, get a document instead of an object tree. The real work is the assertions. Suites built on this renderer typically encode structure — this component tree, these props, this serialised shape. The replacement is not "the same assertion against DOM nodes"; it is a different question: after the user does X, what is on screen? Some of the old assertions have no successor at all, because they were asserting the arrangement of internals and never described a user-visible guarantee. Deleting those is progress, not loss of coverage. The other cost is behaviour that only *appeared* to work. Handlers previously invoked as bare props now go through a real event path, so a control that is disabled, covered, unmounted, or attached to the wrong element starts failing. That is the renderer change doing its job: those tests were green under a fake host and should not have been. ```js // Before: reach into the tree, call the prop yourself. renderer.root.findByType(SaveButton).props.onClick(); // After: find what the user sees, act like the user. await userEvent.click(screen.getByRole('button', { name: 'Save' })); ``` ## How to sequence it on a real codebase Do not rewrite the suite in one change. Convert file by file, starting with the components that carry real interaction, since those are where the fake-host fidelity gap actually hides bugs. Purely presentational components whose tests only ever asserted output shape can wait, or can be retired in favour of a check that runs where appearance is real. Keep the warning visible in CI rather than silencing it — it is the remaining-work counter. And treat each converted file as a chance to ask whether the assertion describes something a user would notice; if it does not, it is not worth porting.
- The team wants to silence the deprecation warning and postpone the migration. What do you tell them?Silencing removes the only visible counter of remaining work, and the warning is not the risk — the fidelity gap is. I would leave it on, treat it as a burn-down list, and convert interaction-heavy components first. If the noise is genuinely blocking triage, scope the suppression to the specific legacy files rather than globally, so newly written tests cannot quietly land on the deprecated path.
- After migrating, several previously green tests fail. How do you tell a real bug from a migration artefact?Ask whether a user could perform the action the test performs. If the new failure is that a control is disabled, hidden, unmounted, or that the handler was on a non-interactive element, the old test was green because it bypassed the host — that is a real defect surfaced. If the failure is about missing environment setup, such as an unimplemented browser API in jsdom, that is an artefact to fix in the test harness.
- Is there any case where you would keep a DOM-less renderer deliberately?Rarely, and not for web. The honest case is a component whose logic must run somewhere with no host at all, or an inherited React Native path not yet migrated. Even then it is a stopgap: anything asserting interaction, focus or appearance is better served by a host that actually implements them, so I would fence its use rather than let it spread.
saying these in an interview costs you the question
- Says deprecated means it stopped working
- Claims the migration is a mechanical render-call swap
- Thinks calling a prop function is equivalent to a click
- Treats a deprecation warning as noise to suppress
- Assumes DOM-level coverage carries over unchanged