skip to content

In React Testing Library, where does render() actually put the component, and what does the library's automatic cleanup do between tests?

level: juniorimportance: should knowfreq 58%

answer

  1. one document, many tests
  2. containers stack up in body
  3. a global afterEach does the work
  4. "found multiple elements" is the tell
  5. DOM only — not mocks or stores

basics

~20 s

render() mounts the component into a fresh div appended to document.body, so tests query real DOM. Automatic cleanup unmounts it after each test, so every test starts from an empty document instead of inheriting the previous test's markup.

solid answer

~50 s

`render(ui)` creates a container `div`, appends it to `document.body`, and mounts the component into it with a real React root, so the assertions run against real DOM nodes rather than a virtual tree. It returns handles for that mount — `container`, `baseElement`, `rerender`, `unmount`, `asFragment` — and `screen` gives you the same queries scoped to `document.body`. Automatic cleanup is the other half: the library registers an `afterEach` hook that unmounts everything it rendered and removes the containers it added. That is what keeps tests independent. Without it, the second test in a file sees both its own markup and the first test's, so a query that should match one element matches two and throws, and effects from the previous component are still running. Cleanup only resets the DOM — mocks, timers, module state and any store you created are still yours to reset.

go deeper

for a junior

Be able to say plainly that render mounts into a real div in document.body and that the library unmounts it after each test, and name the duplicate-element error as the symptom when that does not happen.

for a middle

Explain the mechanics: the container is appended to baseElement, cleanup is registered through a global afterEach, and it resets the DOM only — mocks, timers and stores stay dirty unless you reset them.

for a senior

Show the diagnostic habit — pass-alone-fail-together points at leaked state — and treat unmount as an assertion tool for teardown defects, not just as housekeeping.

for a principal

Own the harness policy: which resets belong in a shared setup file so no test file can forget them, and how you keep test independence a property of the infrastructure rather than of each author's discipline.

## What render() does `render(ui)` is the mount step of a component test. It does three concrete things: 1. Creates a container element — a plain `div` — and appends it to `document.body` (the `baseElement`). 2. Mounts `ui` into that container using a real React root, inside the library's `act()` handling so effects flush. 3. Returns a result object with `container`, `baseElement`, `rerender`, `unmount`, `asFragment`, and query functions bound to that container. The important consequence is that there is nothing virtual about the result. The component's real elements exist in a real (jsdom or browser) document, so you can assert on text, attributes, focus and visibility the way a user's browser would present them. ```js import { render, screen } from '@testing-library/react' test('shows the label', () => { render(<Badge>New</Badge>) // screen queries document.body, which now contains the container div expect(screen.getByText('New')).toBeInTheDocument() }) ``` `screen` is simply the query set bound to `document.body`, which is why it finds anything `render` mounted without you passing the container around. ## Why cleanup exists A test file is one document. Each `render` call appends another container, and nothing removes it on its own — jsdom does not reset the document between tests, and the React root stays mounted with its effects, subscriptions and timers alive. Left alone, a five-test file ends with five copies of your component in the body. That produces the classic symptom: a query fails not because the element is missing but because it matched too many. `getByText` and friends throw when more than one node matches, so test three fails with a "found multiple elements" error that has nothing to do with test three. The library solves this by registering a global `afterEach` hook at import time that calls `cleanup()`. `cleanup` unmounts every root the library mounted and removes every container it added, returning the document to the state the test started in. ```js // conceptually what the library registers for you afterEach(() => { cleanup() // unmount all mounted roots, remove their containers }) ``` ## When it does not happen The registration depends on a global `afterEach` existing at import time. If your runner is configured without global test hooks — for example a runner where `describe`/`it`/`afterEach` are imported explicitly rather than exposed globally — the library has nothing to hook into and auto-cleanup silently does not run. The fix is a setup file that imports `cleanup` and calls it in `afterEach` yourself. There is also a deliberate opt-out (`RTL_SKIP_AUTO_CLEANUP`) for the rare suite that wants to manage mounting itself; needing it is unusual. A good diagnostic instinct: if tests pass individually and the failures are all "found multiple elements" or stale text from a previous case, suspect cleanup before suspecting the component. ## What cleanup does not do Cleanup is DOM-scoped. It does not: - reset module mocks, spies or call counts; - restore fake timers; - reset anything you built outside the render — a store, a cache client, a module-level singleton; - undo global changes the component made, such as a class added to `document.documentElement` by a portal or theme provider, if the component does not remove it on unmount. That last one is worth knowing because it is also a real assertion opportunity: `unmount()` from the render result lets you check that a component tears itself down properly — removes its listeners, aborts its work, clears the body class — rather than leaking. ## What interviewers are really checking The question looks trivial, but it separates candidates who treat the test DOM as a shared, stateful resource from candidates who assume magic isolation. The principle generalises past this one library: any component test mounts into a persistent document, and test independence is something the harness must actively restore between cases. If a suite ever starts failing only when run together, the first hypothesis is leftover state — DOM first, then everything cleanup does not cover.

  • Your runner is configured so test hooks are imported rather than global. What happens to automatic cleanup?
    It never registers. The library attaches its `afterEach` at import time, and if no global `afterEach` exists there is nothing to attach to, so containers accumulate. The fix is a setup file that imports `cleanup` from the library and calls it inside your own `afterEach`, applied to every test file.
  • Beyond keeping tests independent, what is unmount() from the render result useful for asserting?
    Teardown behaviour. Call `unmount()` deliberately and then assert the component cleaned up after itself — removed a document-level listener, cleared a class it added to `document.body`, aborted an in-flight request, stopped an interval. That is a real defect class that a test which only ever mounts will never catch.
  • A test fails with "found multiple elements with the text: Save". What are the two most likely causes?
    Either cleanup is not running so a previous test's markup is still in the document, or the same test rendered twice — commonly a second `render` call instead of `rerender`. Check whether the test passes in isolation: if it does, it is leakage between tests; if it fails alone, the duplicate is inside the test itself.

saying these in an interview costs you the question

  • Thinks each test gets a brand-new document automatically
  • Believes cleanup also resets mocks, timers and stores
  • Adds afterEach(cleanup) everywhere without knowing why
  • Says render returns a virtual tree rather than real DOM
  • Fixes duplicate-match errors by switching to getAllBy and taking [0]

context