Testing with act() and Test Renderer
act() defines a React testing scope: work scheduled inside it is placed in an internal queue, and awaited scopes perform additional flushing before they resolve.
react-test-renderer builds an in-memory container and exposes projections of the resulting Fiber and container state, giving tests inspectable output without requiring the inspected representation to be a browser DOM node.
Sources: packages/react/src/ReactAct.js:31-49, packages/react/src/ReactAct.js:135-159, packages/react-test-renderer/src/ReactTestRenderer.js:511-535, packages/react-test-renderer/src/ReactTestRenderer.js:194-248
Core concepts
Act scope
An act scope is a nestable period in which React routes scheduled work into ReactSharedInternals.actQueue instead of platform scheduling APIs.
actScopeDepth tracks nesting, while didWarnNoAwaitAct records that the missing-await warning has already fired.
Sources: packages/react/src/ReactAct.js:31-49, packages/react/src/ReactAct.js:119-130
Renderer queue
The renderer queue is an array of RendererTask callbacks that flushActQueue executes, including continuations and suspended work.
Sources: packages/react/src/ReactAct.js:311-347
Test container
A test container is a plain object whose children array receives rendered Instance and TextInstance values; create passes it to createContainer and then calls updateContainer.
Sources: packages/react-test-renderer/src/ReactTestRenderer.js:468-631
Test instance
A test instance is a wrapper around an allowed Fiber that exposes current props, type, parent, children, and search helpers.
validWrapperTypes identifies function, class, host, forwarding, memo, simple-memo, and sometimes root tags as eligible wrapper types.
Sources: packages/react-test-renderer/src/ReactTestRenderer.js:299-416, packages/react-test-renderer/src/ReactTestRenderer.js:250-259
How act() drains React work
A synchronous act() increments the scope depth, reuses an existing queue or installs a new one, runs the callback, and conditionally flushes legacy-mode updates before restoring legacy batching state.
If the callback throws, the error is stored, the scope is popped, accumulated errors are aggregated, and the error is rethrown.
An asynchronous callback changes the return contract: act() detects a returned thenable and waits for it to resolve before the outermost scope begins its final queue-draining sequence.
Mechanically, awaited act() waits for the callback’s thenable, a queue flush, and any additional work scheduled by microtasks; it resolves only after the recursive drain observes an empty queue, or rejects with accumulated errors.
The awaited path is summarized here.
Evidence
- act-entrypackages/react/src/ReactAct.js:31
- callbackpackages/react/src/ReactAct.js:58
- act-queuepackages/react/src/ReactAct.js:48
- queue-flusherpackages/react/src/ReactAct.js:311
- async-drainpackages/react/src/ReactAct.js:271
- returned-thenablepackages/react/src/ReactAct.js:135
- test-continuationpackages/react/src/ReactAct.js:300
flushActQueue prevents re-entry with isFlushing, invokes each task, follows non-null continuations, and clears the queue only after the whole queue completes. If a task suspends or throws, remaining work stays represented in the queue or error state.
Nested scopes reuse the queue, but leaving scopes out of order triggers an overlapping-act() warning through popActScope.
If an async act() call is not awaited, queueSeveralMicrotasks schedules a delayed check and emits a warning once; the warning recommends await act(async () =>...).
Sources: packages/react/src/ReactAct.js:42-85, packages/react/src/ReactAct.js:87-101, packages/react/src/ReactAct.js:24-29, packages/react/src/ReactAct.js:104-117, packages/react/src/ReactAct.js:135-163, packages/react/src/ReactAct.js:135-159, packages/react/src/ReactAct.js:271-306, packages/react/src/ReactAct.js:311-347, packages/react/src/ReactAct.js:48-49, packages/react/src/ReactAct.js:256-269, packages/react/src/ReactAct.js:119-130, packages/react/src/ReactAct.js:361-366
How the test renderer builds an inspectable tree
create selects a node mock, determines whether the root is concurrent or legacy when that choice is available, optionally enables strict mode, creates the in-memory container, creates the root, and calls updateContainer.
The renderer’s JSON projection reads the container: it returns null for an absent or empty root, converts a single child recursively, and otherwise converts each child while omitting hidden children.
toTree() instead walks Fibers: class and function components become component records, host tags become host records, text becomes text, and structural tags delegate to their children.
The tree walk handles sibling structure with nodeAndSiblingsArray, then removes nested arrays with flatten.
Thus, the inspected output comes from a plain container object or Fiber-derived records rather than from the browser DOM. The code directly establishes the container shape and the Fiber-to-tree conversion; the DOM comparison is an interpretation of that implementation boundary.
Sources: packages/react-test-renderer/src/ReactTestRenderer.js:468-631, packages/react-test-renderer/src/ReactTestRenderer.js:542-575, packages/react-test-renderer/src/ReactTestRenderer.js:104-144, packages/react-test-renderer/src/ReactTestRenderer.js:194-248, packages/react-test-renderer/src/ReactTestRenderer.js:160-168, packages/react-test-renderer/src/ReactTestRenderer.js:171-192
How tests inspect and mutate the result
A ReactTestInstance keeps its Fiber, resolves its current Fiber through _currentFiber, and reads current memoized props. Its children getter delegates to getChildren, which wraps valid nodes and represents host text as strings.
Search methods share one recursive engine: findAll tests the current instance, optionally stops at a shallow match, and otherwise visits non-string children. findAllByType and findAllByProps provide type and props predicates.
Singular helpers use expectOne, failing when the search returns zero or multiple instances.
| Operation | Handoff | Purpose |
|---|---|---|
create | createContainer, then updateContainer | Establishes and populates the root |
update | updateContainer(newElement, root, null, null) | Replaces the rendered element |
unmount | updateContainer(null, root, null, null) | Removes the tree and clears references |
These operations form the renderer’s root lifecycle.
wrapFiber caches one ReactTestInstance per Fiber in fiberToWrapper, checking the alternate Fiber before allocating a new wrapper.
Sources: packages/react-test-renderer/src/ReactTestRenderer.js:299-368, packages/react-test-renderer/src/ReactTestRenderer.js:261-297, packages/react-test-renderer/src/ReactTestRenderer.js:418-441, packages/react-test-renderer/src/ReactTestRenderer.js:399-415, packages/react-test-renderer/src/ReactTestRenderer.js:371-390, packages/react-test-renderer/src/ReactTestRenderer.js:443-457, packages/react-test-renderer/src/ReactTestRenderer.js:468-631, packages/react-test-renderer/src/ReactTestRenderer.js:582-597, packages/react-test-renderer/src/ReactTestRenderer.js:633, packages/react-test-renderer/src/ReactTestRenderer.js:634-644
Why updates outside act() warn
A non-null ReactSharedInternals.actQueue signals that work is inside an act() scope; React routes scheduled tasks into that queue instead of platform APIs.
The reconciler warns when a test update occurs while that queue is null. The warning instructs tests to wrap state-update-causing code in act() before asserting output.
The warning therefore marks a missing test boundary: the update happened when React was not inside the queue-based scope that act() establishes and flushes.
Sources: packages/react/src/ReactAct.js:31-49, packages/react-reconciler/src/ReactFiberWorkLoop.js:5635-5651, packages/react/src/ReactAct.js:31-101
How it connects
The testing boundary belongs to react, while the in-memory renderer belongs to react-test-renderer; the renderer aliases React.act as its local act constant.
The renderer hands root updates to the reconciler through createContainer and updateContainer, then exposes container and Fiber projections through toJSON, toTree, and ReactTestInstance.
For broader context, see React Overview, Fiber Architecture, and Jest Test Infrastructure
Sources: packages/react-test-renderer/src/ReactTestRenderer.js:71, packages/react-test-renderer/src/ReactTestRenderer.js:468-631, packages/react-test-renderer/src/ReactTestRenderer.js:542-580, packages/react-test-renderer/src/ReactTestRenderer.js:299-416, packages/react/src/ReactAct.js:31-49
Key takeaways
act()captures work in ReactSharedInternals.actQueue and awaited scopes keep checking asynchronous tasks before resolving.react-test-rendererrenders into an in-memorycontainerand exposes JSON, Fiber-derived trees, and test instances.- Async
act()must be awaited to avoid missing-await and overlapping-scope warnings. - Updates outside an active act queue warn because the test has not declared the boundary that should contain and flush them.
Sources: packages/react/src/ReactAct.js:31-49, packages/react/src/ReactAct.js:271-306, packages/react-test-renderer/src/ReactTestRenderer.js:468-631, packages/react-test-renderer/src/ReactTestRenderer.js:194-248, packages/react-test-renderer/src/ReactTestRenderer.js:299-416, packages/react/src/ReactAct.js:119-130, packages/react/src/ReactAct.js:256-269, packages/react-reconciler/src/ReactFiberWorkLoop.js:5635-5651