In React Native Testing Library 14, why can getByText still miss a list loaded by a mocked fetch after await render(), and what should the test use instead?
answer
- render finishes React work, not your promise
- getBy is one synchronous check
- findBy is getBy plus waitFor
- retries every 50 ms for 1000 ms
- await the findBy, then plain getBy
basics
~20 sAwaiting RNTL 14's render() completes the mount but does not wait for a mocked fetch to resolve, and getByText checks the tree once. Use await screen.findByText(), which retries every 50 ms until the item appears or 1000 ms pass.
solid answer
~40 sIn RNTL 14 `render` is async: awaiting it lets React finish the mount and flush the updates already queued, which is what makes Suspense and `use()` testable. It knows nothing about the `fetchBooks` promise your effect started, so whether the titles are in the tree on the next line is a matter of timing, not something the test guarantees. `getByText` is one synchronous check that throws when nothing matches. The fix is `expect(await screen.findByText('Dune')).toBeOnTheScreen()`: a `findBy*` query is a `getBy*` wrapped in `waitFor`, re-run every 50 ms until it matches or 1000 ms pass, and its promise has to be awaited. When the whole list arrives in one state update, the assertions after that first `findBy` can use plain `getBy*`.
code
typescript · 17 linesimport { render, screen } from '@testing-library/react-native';
import { CatalogScreen } from './CatalogScreen';
import { fetchBooks } from './api';
jest.mock('./api');
const mockFetchBooks = jest.mocked(fetchBooks);
test('shows the loaded catalog', async () => {
mockFetchBooks.mockResolvedValue([
{ id: 'b1', title: 'Dune' },
{ id: 'b2', title: 'Emma' },
]);
await render(<CatalogScreen />);
expect(await screen.findByText('Dune')).toBeOnTheScreen();
expect(screen.getByText('Emma')).toBeOnTheScreen();
});go deeper
Recall that getBy checks once, findBy waits, and that both render and findBy must be awaited in RNTL 14. Know the 1000 ms default timeout.
Explain that findBy is getBy run through waitFor at a 50 ms interval, and why await render() flushes React's queued work but not the component's own promises.
Show how you keep async tests deterministic: mock at the network boundary, wait on the first user-visible consequence, and assert the rest synchronously so failures point at the real async step.
Frame the team convention: lint rules for awaited async utilities, one shared data-mocking layer, and a rule that no test sleeps, so async flakiness is prevented by construction.
## The failing test A library-catalog screen, `CatalogScreen`, calls `fetchBooks()` inside `useEffect`, stores the result with `setBooks`, and renders the titles in a `FlatList`. The test mocks the API module so `fetchBooks` resolves with two books, awaits `render(<CatalogScreen />)`, and asserts `screen.getByText('Dune')`. Sometimes it passes, sometimes it throws "Unable to find an element with text: Dune", and it fails every time once the mock is given a small delay. ## What `await render()` does in RNTL 14 **React Native Testing Library (RNTL)** 14 made `render` asynchronous. It returns a Promise because it runs the mount inside React 19's async `act`, which lets React finish rendering, run the effects of that commit and flush the updates that are already queued before your next line runs. That is what makes components that suspend, or that call `use()`, testable. What `await render()` does **not** do is wait for work your component started and React knows nothing about: - the `fetchBooks()` promise created in the effect; - the `res.json()` call chained on it; - a `setTimeout`, a debounce or an animation the screen schedules; - anything that finishes after an unknown number of microtask or timer turns. So the state update that puts the books in the tree may or may not have happened by the time the next line executes. A test that passes today because the mock resolves within the turns React happened to flush is relying on an implementation detail of the mock. This is a common misreading after a v13 to v14 upgrade. In v13 `render` was synchronous, and teams that ran the `rntl-v14-async-functions` codemod now see `await render(...)` everywhere and assume the `await` covers data loading too. It covers React's own work, which is why a component that suspends on a promise passed to `use()` is handled, but a plain `useEffect` fetch followed by `setState` is ordinary application code that React does not track. ## The query families and which one waits | Query | Returns | When nothing matches | Waits? | |---|---|---|---| | `getBy*` | the element | throws at once | no | | `queryBy*` | the element or `null` | returns `null` | no | | `findBy*` | a Promise of the element | rejects after the timeout | yes | `getByText` is a **single synchronous check** of the rendered tree. `findByText` is that same check run through `waitFor`: RNTL calls the `getBy` query, and while it throws, calls it again every **50 ms** (`interval`) until it succeeds or **1000 ms** (`timeout`) have passed. On success the promise resolves with the element; on timeout it rejects with the last "Unable to find" error, with the rendered element tree appended so you can see what was on screen. ## Writing the catalog test 1. Mock the network boundary so the test controls the data. 2. Await `render`. 3. Await the first thing that depends on the async work with a `findBy*` query. 4. Assert the rest synchronously, since it arrived in the same update. ```tsx test('shows the loaded catalog', async () => { mockFetchBooks.mockResolvedValue([ { id: 'b1', title: 'Dune' }, { id: 'b2', title: 'Emma' }, ]); await render(<CatalogScreen />); expect(await screen.findByText('Dune')).toBeOnTheScreen(); expect(screen.getByText('Emma')).toBeOnTheScreen(); }); ``` The second assertion needs no waiting because `setBooks` put both titles in the tree in one update: once `Dune` is visible, `Emma` is too. If the screen loaded data in two separate steps, the second step would need its own `findBy*`. ## Mistakes interviewers listen for - **Believing `await render()` waits for effects' network calls.** It flushes React work, not your promises. - **Dropping the `await` on `findBy*`.** The query then returns a pending promise, `expect` receives a Promise instead of an element, and the test can end before the wait settles. - **Padding with sleeps.** A `setTimeout`-based pause or an empty `await act(async () => {})` encodes a guess about how many turns the mock needs; it breaks as soon as the mock or the fetch chain changes. - **Using `findBy*` for every later assertion.** It is harmless, but it hides which step was actually asynchronous and slows failures down to the full timeout. - **Reaching for `waitFor(() => screen.getByText(...))`.** It works, but `findBy*` is the purpose-built form and produces a better error. The general rule: `await render()` for the mount, `findBy*` for the first thing that depends on asynchronous work, plain `getBy*` or `queryBy*` afterwards.
- What goes wrong if the test calls screen.findByText('Dune') without await?The query returns a pending promise, so `expect` receives a Promise rather than an element and the assertion is meaningless. The test body can also finish while the wait is still polling; RNTL's cleanup then unmounts the screen and aborts the pending wait, and the resulting error points at an un-awaited `findBy*` or `waitFor`. The `await-async-utils` rule of eslint-plugin-testing-library catches the related `waitFor` mistake at lint time.
- Why not add `await act(async () => {})` after render and keep getByText?An empty async `act` drains whatever is queued at that moment. Whether the mocked fetch, its `json()` step and the state update all complete inside that window depends on how the mock is written, so the test encodes a guess about timing. A `findBy*` query states the actual condition, the title being on screen, and keeps working when the fetch chain or the mock changes.
saying these in an interview costs you the question
- Awaiting render() in RNTL 14 waits for every effect's network call to finish.
- getByText keeps retrying until the element appears.
- findByText works fine without await because it is still a query.
- Add a setTimeout pause after render so the fetch has time to finish.
- Every assertion after the first findBy must also use findBy.