You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Unable to test components that suspend when using React 19 #1787
When using React 19 and testing a component that suspends, the component never reaches its un-suspended state and the test fails with the following error:
console.error
A component suspended inside an `act` scope, but the `act` call was not awaited. When testing React components that depend on asynchronous data, you must await the result:
await act(() => ...)
at node_modules/react/cjs/react.development.js:1157:23
Expected behavior
The component should be able to un-suspend, allowing the test to run properly and pass.
Steps to Reproduce
After cloning the reproduction repository (a fork with a test case added) and installing dependencies, run the tests (yarn run test).
For context, the actual issue the MCVE reproduces was encountered during an upgrade of Expo from 52 to 53, which upgrades React from 18 to 19. Tests passed with React 18 but did not with React 19.
In case they are significant, here are some notes regarding the similarities and differences between the actual issue the MCVE reproduces and the MCVE.
Similarities:
The tests fail.
The render tree shows the Suspense fallback – the component is never rendered in its un-suspended state.
Differences:
In the actual issue, the components under test did not use use, instead they used a Suspense-enabled library (React Query). The MCVE does not use React Query to keep the reproduction as minimal as possible.
In the actual issue, tests did not call render from @testing-library/react-native directly – they called renderRouter from expo-router/testing-library which under-the-hood calls render from @testing-library/react-native. The MCVE does not use Expo Router to keep the reproduction as minimal as possible.
In the actual issue, the error "a component suspended inside an act scope, but the act call was not awaited" is never logged. No errors are thrown (only test expectations fail). This point could mean this MCVE doesn't reproduce the actual issue however the similarities are close enough.
A contributor to React Testing Library gave the following workaround (comment):
In the meantime, you can unblock yourselves by wrapping the updates that suspend in await act(async () => { ... }). The passed callback to act must be async in those cases.
However, in React Native Testing Library, it causes a different error:
In regards to the last point of the additional notes:
In the actual issue, the error "a component suspended inside an act scope, but the act call was not awaited" is never logged. No errors are thrown (only test expectations fail). This point could mean this MCVE doesn't reproduce the actual issue however the similarities are close enough.
I've added an additional example using Expo Router Testing Library. It's on the with-expo branch (diff with main).
The difference renderRouter from Expo Router Testing Library brings seems to be calling jest.useFakeTimers before render (source). In the example on main, adding a call to jest.useFakeTimers before render stops the error ("a component suspended inside an act scope, but the act call was not awaited") from being thrown (but the test still fails).
Hmm, I seem to have been misunderstanding React.use. From adding logs, it looks like Suspendable was calling the async function on every render which causes the component to suspend indefinitely because the Promise gets re-created. React.use under-the-hood does not keep a stable reference to the Promise. I've (now) noticed the documentation also states this:
Promises created in Client Components are recreated on every render.
I imagine a Suspense-enabled library caches a stable reference to a Promise under-the-hood. I did not investigate this further but decided to pass in the Promise as a prop:
// @ts-expect-error `TestRenderer.create` is not typed correctly
renderer=TestRenderer.create(component,options);
});
// @ts-expect-error: `act` is synchronous, so `renderer` is already initialized here
returnrenderer;
}
This experimentation started because I first experimented with converting synchronous act calls to asynchronous act calls but did not get anywhere successfully – the component still never seemed to render in its un-suspended state. As a result I decided to switch track and see what would get a successful test pass with minimal React Native Testing Library internals. The test case below passes and may help with determining the final solution:
test('react test renderer supports components which can suspend',async()=>{functionwait(delay: number){returnnewPromise<void>((resolve)=>setTimeout(()=>{resolve();},delay),);}functionSuspendable<T>({ promise }: {promise: Promise<T>}){React.use(promise);return<ViewtestID="test"/>;}functionFallback(){return<ViewtestID="fallback"/>;}letrenderer: ReactTestRenderer;awaitReact.act(async()=>{renderer=TestRenderer.create(<React.Suspensefallback={<Fallback/>}><Suspendablepromise={wait(1000)}/></React.Suspense>,);});letview=within(renderer!.root);expect(view.getByTestId('fallback')).toBeDefined();awaitwaitFor(()=>{view=within(renderer!.root);view.getByTestId('test');});expect(view.getByTestId('test')).toBeDefined();});
An additional lesson: Suspense at the root causes a problem where the root gets un-mounted when the Promise resolves and the un-suspended component gets rendered. This is why the test case in the previous message needed the queries to be 're-bound' to the root:
awaitwaitFor(()=>{view=within(renderer!.root);// <-- queries 're-bound' to rootview.getByTestId('test');});
Below is the test case updated to have a View at the root. This removes the need for the above waitFor:
test('react test renderer supports components which can suspend',async()=>{functionwait(delay: number){returnnewPromise<void>((resolve)=>setTimeout(()=>{resolve();},delay),);}functionSuspendable<T>({ promise }: {promise: Promise<T>}){React.use(promise);return<ViewtestID="test"/>;}functionFallback(){return<ViewtestID="fallback"/>;}letrenderer: ReactTestRenderer;awaitReact.act(async()=>{renderer=TestRenderer.create(<View><React.Suspensefallback={<Fallback/>}><Suspendablepromise={wait(1000)}/></React.Suspense></View>,);});letview=within(renderer!.root);expect(view.getByTestId('fallback')).toBeDefined();expect(awaitview.findByTestId('test')).toBeDefined();});
In the final solution, however, I imagine the possibility of having a Suspense component at the root should be handled.
The commit above is a demo of making render asynchronous. The test case for rendering components also has a similar problem where it's flaky when run in isolation and always fails when run with other tests. However the failure is slightly different:
Unable to find an element with testID: test
<View>
<View
testID="test"
/>
</View>
An error of being unable to find an element with a given test ID then printing a tree with the element with the given test ID is baffling 🤔
@winghouchan Thanks for thorough investigation. Regarding your test using RTR + await act the reason for flakyness seems to be that you hit the findBy* timeout limit which is 1000 ms, if you either reduce wait time or increase the timeout duration the thigs seem to work better:
@winghouchan, @mikelros I've just released RNTL v13.3 (beta) with React Suspense support. 🎉 It adds renderAsync, fireEventAsync, screen.rerenderAsync, etc variants that should work correctly with React Suspense.
Could you test it on your React Suspense code and report back if it works correctly?
@winghouchan@mikelros any updates from you? I would like to make this API stable soon, and your feedback whether it works for you would be very helpful in go/no-go decision.
Activity
winghouchan commentedon Jul 11, 2025
Additional notes
For context, the actual issue the MCVE reproduces was encountered during an upgrade of Expo from 52 to 53, which upgrades React from 18 to 19. Tests passed with React 18 but did not with React 19.
In case they are significant, here are some notes regarding the similarities and differences between the actual issue the MCVE reproduces and the MCVE.
Similarities:
Differences:
use, instead they used a Suspense-enabled library (React Query). The MCVE does not use React Query to keep the reproduction as minimal as possible.renderfrom@testing-library/react-nativedirectly – they calledrenderRouterfromexpo-router/testing-librarywhich under-the-hood callsrenderfrom@testing-library/react-native. The MCVE does not use Expo Router to keep the reproduction as minimal as possible.actscope, but theactcall was not awaited" is never logged. No errors are thrown (only test expectations fail). This point could mean this MCVE doesn't reproduce the actual issue however the similarities are close enough.winghouchan commentedon Jul 11, 2025
React Testing Library seems to have a similar issue: testing-library/react-testing-library#1375
winghouchan commentedon Jul 11, 2025
A contributor to React Testing Library gave the following workaround (comment):
However, in React Native Testing Library, it causes a different error:
winghouchan commentedon Jul 11, 2025
In regards to the last point of the additional notes:
I've added an additional example using Expo Router Testing Library. It's on the
with-expobranch (diff withmain).winghouchan commentedon Jul 11, 2025
The difference
renderRouterfrom Expo Router Testing Library brings seems to be callingjest.useFakeTimersbeforerender(source). In the example onmain, adding a call tojest.useFakeTimersbeforerenderstops the error ("a component suspended inside anactscope, but theactcall was not awaited") from being thrown (but the test still fails).mdjastrzebski commentedon Jul 11, 2025
@winghouchan in your repro repo, would using
await findByTestIdinstead ofgetByTestIdwould make the test pass?winghouchan commentedon Jul 11, 2025
@mdjastrzebski: no, that made no difference, the same things were experienced.
Code in the repo (commit) and the original report have been updated.
winghouchan commentedon Jul 11, 2025
Note
The reproduction has been updated to a test case added to the library's tests. See diff between the fork and the upstream.
winghouchan commentedon Jul 12, 2025
Hmm, I seem to have been misunderstanding
React.use. From adding logs, it looks likeSuspendablewas calling the async function on every render which causes the component to suspend indefinitely because the Promise gets re-created.React.useunder-the-hood does not keep a stable reference to the Promise. I've (now) noticed the documentation also states this:–
useCaveats, React DocumentationInterestingly, putting the Promise in state (example below) does not seem to yield the desired effect.
waitstill seems to be called on every render.I imagine a Suspense-enabled library caches a stable reference to a Promise under-the-hood. I did not investigate this further but decided to pass in the Promise as a prop:
This enabled experimentation with
actand React Test Renderer'screatemethod, which React Native Testing Library calls under-the-hood:react-native-testing-library/src/render-act.ts
Lines 6 to 20 in 0f5cc2e
This experimentation started because I first experimented with converting synchronous
actcalls to asynchronousactcalls but did not get anywhere successfully – the component still never seemed to render in its un-suspended state. As a result I decided to switch track and see what would get a successful test pass with minimal React Native Testing Library internals. The test case below passes and may help with determining the final solution:winghouchan commentedon Jul 12, 2025
An additional lesson:
Suspenseat the root causes a problem where the root gets un-mounted when the Promise resolves and the un-suspended component gets rendered. This is why the test case in the previous message needed the queries to be 're-bound' to the root:Below is the test case updated to have a
Viewat the root. This removes the need for the abovewaitFor:In the final solution, however, I imagine the possibility of having a
Suspensecomponent at the root should be handled.winghouchan commentedon Jul 12, 2025
winghouchan commentedon Jul 12, 2025
fix: make render function asynchronous (WIP)
winghouchan commentedon Jul 13, 2025
The commit above is a demo of making
renderasynchronous. The test case for rendering components also has a similar problem where it's flaky when run in isolation and always fails when run with other tests. However the failure is slightly different:An error of being unable to find an element with a given test ID then printing a tree with the element with the given test ID is baffling 🤔
mdjastrzebski commentedon Jul 14, 2025
@winghouchan Thanks for thorough investigation. Regarding your test using RTR +
await actthe reason for flakyness seems to be that you hit thefindBy*timeout limit which is 1000 ms, if you either reduce wait time or increase the timeout duration the thigs seem to work better:Pls note, I'm still all other mentioned issues.
15 remaining items
build(expo): upgrade expo sdk to version 53
mikelros commentedon Aug 4, 2025
@mdjastrzebski could you please provide an update on the ETA for this? Thank you for the effort.
mdjastrzebski commentedon Aug 4, 2025
I still have to solve fake timers, as the PR only supports real timers. Would having just real timers support be ok with you?
mdjastrzebski commentedon Aug 7, 2025
@winghouchan, @mikelros I've just released RNTL v13.3 (beta) with React Suspense support. 🎉 It adds
renderAsync,fireEventAsync,screen.rerenderAsync, etc variants that should work correctly with React Suspense.Could you test it on your React Suspense code and report back if it works correctly?
devtoil commentedon Aug 7, 2025
Works for me! Thanks!
mdjastrzebski commentedon Aug 12, 2025
@winghouchan @mikelros any updates from you? I would like to make this API stable soon, and your feedback whether it works for you would be very helpful in go/no-go decision.
winghouchan commentedon Aug 12, 2025
@mdjastrzebski: thanks for your efforts! Sorry, I have seen your message, just haven't got round to testing it yet! I expect to this week.
test: upgrade react native testing library to version supporting test…
test: upgrade react native testing library to version supporting test…
winghouchan commentedon Aug 15, 2025
@mdjastrzebski: all my tests pass with the 13.3 beta ✅ Happy to call this resolved 😄 Thank you!
mikelros commentedon Aug 21, 2025
Everything working fine @mdjastrzebski 👌🏼 thanks for the effort!
test: upgrade react native testing library to version supporting test…