Back to KB
Difficulty
Intermediate
Read Time
8 min

`act` vs. `waitFor`

By Codcompass TeamΒ·Β·8 min read

Synchronizing React Tests: A Practical Guide to act and waitFor

Current Situation Analysis

Modern React applications rely heavily on asynchronous side effects: data fetching, debounced inputs, WebSocket streams, and scheduled UI updates. When testing these components, developers frequently encounter two recurring failures: the cryptic Warning: An update to X inside a test was not wrapped in act(...) message, and TestingLibraryElementError: Unable to find an element timeouts. These failures rarely stem from broken component logic. They stem from a synchronization mismatch between React's rendering scheduler and the test runner's execution cycle.

The core problem is often overlooked because testing libraries abstract away the rendering lifecycle. Functions like render, fireEvent, and userEvent silently wrap their operations in act, creating a false sense of security. Developers assume the test environment automatically handles all asynchronous boundaries. In reality, act only controls React's internal update queue. It has zero authority over browser APIs like fetch, setTimeout, IntersectionObserver, or custom promise chains. When a test triggers a network request or a timer, React's scheduler completes its cycle before the external operation resolves. The test runner proceeds to assertions while the DOM remains in its initial state, triggering flaky failures or the infamous act warning.

Industry telemetry and framework documentation consistently show that over 70% of reported React test flakiness originates from improper handling of async boundaries. The React team explicitly designed act to flush pending state updates before assertions run. Testing Library's own guidelines emphasize that queries and renders are already synchronized. The gap appears when developers attempt to test custom hooks, fake timers, or unmocked network calls without understanding which layer (React scheduler vs. browser runtime) controls the operation. Recognizing this boundary is the difference between deterministic, fast tests and fragile, timeout-prone suites.

WOW Moment: Key Findings

The confusion between act and waitFor disappears when you map their execution models against React's rendering cycle and browser runtime boundaries. The following comparison reveals why certain approaches succeed while others introduce race conditions.

ApproachExecution ModelRetry BehaviorAsync Boundary
act()Synchronous flushNoneReact scheduler only
waitFor()Polling callbackBuilt-in retry loopExternal promises & timers
findBy*Query wrapperImplicit waitForDOM appearance after async ops

Why this matters: act forces React to process its internal update queue synchronously. It does not wait for network responses, timer callbacks, or third-party library resolutions. waitFor operates entirely outside React's scheduler; it repeatedly executes a callback until the assertion passes or the timeout expires. findBy* queries are syntactic sugar that combine DOM querying with waitFor. Understanding these boundaries eliminates guesswork. You stop wrapping network calls in act, stop using getBy for async content, and start aligning your synchronization strategy with the actual source of the asynchronous operation.

Core Solution

Testing React components deterministically requires matching the synchronization primitive to the operation's origin. Below is a production-ready implementation demonstrating how to handle direct state mutations, network requests, and scheduled timers using act and waitFor.

1. Direct State Mutations: The act Boundary

When testing custom hooks or compone

πŸŽ‰ Mid-Year Sale β€” Unlock Full Article

Base plan from just $4.99/mo or $49/yr

Sign in to read the full article and unlock all 635+ tutorials.

Sign In / Register β€” Start Free Trial

7-day free trial Β· Cancel anytime Β· 30-day money-back