Join our Newsletter — 33% off our NHI Course

What are the signs that a React 18 migration is breaking existing component tests?

Common signs include tests timing out, assertions finding empty content on the first render, and warnings around act() that start turning into failures. Another indicator is user-event tests hanging when fake timers are enabled. These symptoms usually point to outdated assumptions about render timing, so the fix often involves async queries, timer setup, or removing redundant act wrappers.

What changes when React 18 breaks a test suite

The most useful way to read these failures is as a timing mismatch, not as random test noise. React 18’s concurrent rendering and stricter effect behavior can expose tests that were accidentally relying on synchronous updates, immediate DOM availability, or cleanup order that no longer holds.

When that happens, the breakage usually clusters around the same few patterns: the component renders, but the expected content is not there yet; the assertion runs before the UI settles; or the test harness is making assumptions about state changes that now arrive asynchronously. If the failures are widespread after the migration, that is a strong signal the tests are coupled to implementation timing rather than user-observable behavior.

  • Assertions that passed on the first render now need a wait step or a query that retries.
  • Components that looked stable under React 17 now produce empty or partial DOM snapshots at the moment of assertion.
  • State updates triggered by user interactions or effects no longer complete before the test continues.

Why act() warnings and fake timers are such common tripwires

Two recurring indicators deserve special attention because they usually point to a test harness problem rather than a product bug. act() warnings often mean an update is escaping the test’s expectation boundary, while hangs around user-event plus fake timers usually mean the test environment is controlling time in a way the interaction library cannot safely complete.

In practice, that means the migration did not just reveal a broken assertion, it revealed that the test is not faithfully modeling how the component updates. If a test only passes when you add redundant act wrappers, advance timers manually at the right moment, or swap a synchronous assertion for a retrying one, the old test was depending on behavior React 18 no longer guarantees.

  • act() warnings that escalate into failures usually mean the update lifecycle needs to be awaited, not wrapped again.
  • Fake timers can block internal delays, microtasks, or user-event sequencing unless the test is configured carefully.
  • Timeouts often indicate the test is waiting for a condition that never becomes true under the new render model.

What to verify before you treat a failure as a real regression

First, verify whether the component is actually failing or whether the test is asserting too early. The most reliable check is to compare the user-visible state you expect with the exact moment the assertion runs, especially after clicks, typing, effects, or async data loading.

Then look for stale test patterns: direct DOM reads where a retrying query is needed, unnecessary act wrappers, or timer setup that prevents asynchronous work from finishing. A React 18 migration often exposes more than one issue at once, so the fix is usually a combination of waiting correctly, aligning timer behavior, and reducing assumptions about synchronous rendering.

  • Prefer queries that reflect eventual user-visible state when the UI updates asynchronously.
  • Review fake timer usage in any test that simulates typing, debouncing, intervals, or delayed effects.
  • Remove extra wrappers or helpers that were compensating for old render timing.

Practitioner Guidance

What to prioritise: Triage failures by symptom type, timeouts, empty first render assertions, act() warnings, and user-event hangs. That grouping usually reveals whether the fix belongs in the test query, the timer model, or the interaction setup.

What to verify: Confirm the test is waiting for the same observable state a user would see, not an internal state transition. If a test only becomes stable after adding more synchronization scaffolding, treat that as a sign the test design still needs refactoring.

Common mistake: Do not normalize away the warnings by piling on extra wrappers. In React 18, that often hides the real issue and leaves you with slower tests that still encode the wrong timing assumptions.

Practitioner takeaway: The strongest signal of a bad migration test is not that it fails, but that it fails for timing reasons that a user would never notice; fix the test to observe settled behavior, not implementation speed.