skip to content

In React Native Testing Library 14, how do you render a component and find an element through screen, and why must render be awaited?

level: juniorimportance: must knowfreq 58%

answer

  1. render returns a Promise in v14
  2. screen points at the latest render
  3. getByText matches host Text only
  4. getBy throws on zero or several
  5. cleanup runs after each test

basics

~10 s

In RNTL 14 you write await render(<LoginScreen />), then query with screen.getByText('Welcome back'). render is async because it wraps rendering in an async act, and screen is only bound after that promise resolves.

solid answer

~40 s

React Native Testing Library 14 renders the component with Test Renderer into an in-memory tree of host elements; no simulator and no native code are involved. `render()` now returns a Promise because it runs the render inside an async `act`, which lets React 19 finish pending work such as `Suspense` and `use()`. Only when that promise resolves does the library bind `screen` to the new tree, so the test must be `async` and must `await render(...)`. Then `screen.getByText('Welcome back')` returns the single host `Text` whose content matches, and throws if there are none or several. `getAllBy*` returns a non-empty array and `queryBy*` returns `null` when nothing matches. After each test an automatic cleanup unmounts the tree and resets `screen`.

code

tsx · 9 lines
tsx
import { render, screen } from '@testing-library/react-native';
import { LoginScreen } from './LoginScreen';

test('shows the sign-in heading', async () => {
  await render(<LoginScreen />);

  expect(screen.getByText('Welcome back')).toBeOnTheScreen();
  expect(screen.queryByText('Session expired')).not.toBeOnTheScreen();
});

go deeper

for a junior

Recall the three steps: an async test, await render, then a screen query such as getByText, which throws when it finds nothing.

for a middle

Explain why render became async in v14, when screen is bound and reset, and the zero, one and many behaviour of each variant.

for a senior

Diagnose suites migrated from v13: missing awaits, destructured Promises and tests passing on unflushed trees, and apply the codemod with review.

for a principal

Set conventions for a large suite: screen as the only access style, lint rules for floating promises, and a pinned Test Renderer line matched to the React version.

## What render does in v14 React Native Testing Library (RNTL) does not run the React Native renderer, because that renderer needs iOS or Android. Since v14 it uses **Test Renderer**, the successor of the deprecated `react-test-renderer`, which renders components to plain JavaScript objects inside Jest on Node. The result is a tree of **host elements** (`View`, `Text`, `TextInput`, `Image` and so on) that mirrors what would reach the native view hierarchy. RNTL 14 requires React 19 and React Native 0.78 or later, and it adopted React 19's async rendering model: - `render()` returns `Promise<RenderResult>`; - `rerender()` and `unmount()` return `Promise<void>`; - the initial render runs inside an async `act`, so pending updates, `Suspense` boundaries and the `use()` hook settle before the promise resolves. ## screen versus the render result | Access style | Example | Notes | |---|---|---| | `screen` object | `screen.getByText('Welcome back')` | Bound to the most recent render; the recommended style | | Destructured result | `const { getByText } = await render(...)` | Same queries, tied to that render call | `screen` is assigned **after** the render promise resolves and is reset by the automatic cleanup that RNTL registers with `afterEach`. Before any render in a test, every `screen` method throws an error saying that `render` has not been called. ## Finding text with getByText - `*ByText` matches only **host `Text` elements**, by their text content. - Nested `Text` children are joined, so `<Text>Hello <Text>Ana</Text></Text>` matches `'Hello Ana'`, as React Native renders them together. - Matching is **exact by default**: whole string, case-sensitive. Pass a regular expression such as `/welcome/i` for fuzzy matching; `{ exact: false }` also works but regex is preferred. - Text is **normalized** first: leading and trailing whitespace trimmed, inner runs of whitespace collapsed. ## The variants in RNTL | Variant | Zero matches | One match | Several matches | |---|---|---|---| | `getBy*` | throws | returns it | throws | | `getAllBy*` | throws | array of one | array | | `queryBy*` | `null` | returns it | throws | | `queryAllBy*` | `[]` | array | array | Each returns a **`TestInstance`**, an object with a string `type`, `props`, `parent` and `children`. Waiting for elements that appear later is the job of the `findBy*` variants. ## What goes wrong without await 1. `render(<LoginScreen />); screen.getByText('Sign in')` throws because `screen` has not been bound yet. 2. `const { getByText } = render(...)` destructures a Promise, so `getByText` is `undefined` and the call fails with a type error. 3. A test that happens to pass may assert on a tree whose effects have not been flushed. The v14 migration provides a codemod that adds these `await`s, but new tests should be written async from the start.

  • When would you destructure queries from the render result instead of using screen?
    Rarely. `screen` always points at the latest render and keeps tests uniform. Destructuring from `await render(...)` is equivalent and can help when a helper returns the render result, but mixing both styles in one suite makes tests harder to read.
  • Why does getByText('Welcome') fail when the Text says 'Welcome back'?
    String matching is exact by default: the whole normalized string, case-sensitive. Use the full text, or a regular expression such as `/welcome/i` to match a substring regardless of case. `{ exact: false }` also works, but the docs prefer regex for fuzzy matching.

saying these in an interview costs you the question

  • render is still synchronous in RNTL 14, so await is optional.
  • RNTL renders the app on a simulator in the background.
  • getByText returns the first match when several elements match.
  • screen keeps the previous test's tree until the next render.
  • getByText can find text typed into a TextInput.