You are viewing documentation for v14 RC.

Migration to 14.x

This guide describes the migration to React Native Testing Library version 14 from version 13.x.

Overview

RNTL v14 drops support for React 18 and adopts React 19's async rendering model. Here's what changed:

  • React 19.0.0+ and React Native 0.78+ are now required
  • Node.js ^22.13.0 || >=24 is now required
  • render, renderHook, fireEvent, and act are now async
  • Switched from deprecated React Test Renderer to Test Renderer
  • Removed deprecated APIs: update, getQueriesForElement, UNSAFE_root, concurrentRoot option, createNodeMock option
  • Reintroduced container API, which is now safe to use
React 18 Users

If you need to support React 18, please continue using RNTL v13.x.

Quick Migration

We provide codemods to automate most of the migration:

Step 1: Update dependencies

npm
yarn
pnpm
bun
npx codemod@latest rntl-v14-update-deps --target .
npm install

Step 2: Update test code to async

npm
yarn
pnpm
bun
npx codemod@latest rntl-v14-async-functions --target ./src

After running the codemods, review the changes and run your tests.

If your project uses coding agents, add the RNTL package-docs instruction snippet from the Quick Start so agents read the docs that match your installed package version.

Breaking Changes

Supported React, React Native, and Node.js versions

This version requires React 19+, React Native 0.78+, and Node.js ^22.13.0 || >=24. If you need to support React 18, please use the latest v13.x version.

RNTL VersionReact VersionReact Native VersionNode.js Version
v14.x>= 19.0.0>= 0.78^22.13.0 || >=24
v13.x>= 18.0.0>= 0.71>= 18

Test Renderer replaces React Test Renderer

In v14, React Native Testing Library uses Test Renderer instead of the deprecated React Test Renderer. Test Renderer works with React 19 and has better TypeScript support.

What changed:

  • The underlying renderer is now Test Renderer instead of React Test Renderer
  • This is mostly an internal change; your tests should work without modifications in most cases
  • Type definitions now use TestInstance from Test Renderer instead of ReactTestInstance
  • Test Renderer 1.x is required as a peer dependency. Use the recommended Test Renderer version for your React 19 minor version.

Choose the Test Renderer version that matches your React 19 minor version:

React versionRecommended Test Renderer versionNotable React features
19.2[email protected]<Activity />, useEffectEvent
19.1[email protected]Owner Stack support, updated useId() format
19.0[email protected]Actions, useActionState, useOptimistic, use

Using an older Test Renderer line can prevent RNTL from supporting newer React 19 features in your tests. Using a newer Test Renderer line than your React version can produce peer dependency warnings, or an install error with npm.

See the Test Renderer React 19 compatibility lines for the latest recommendations.

Migration:

1. Update dependencies

Run codemod for updating dependencies:

npm
yarn
pnpm
bun
npx codemod@latest rntl-v14-update-deps
npm install
Manual changes

Remove React Test Renderer and its type definitions from your dev dependencies, and add the Test Renderer line that matches your React minor version:

npm
yarn
pnpm
bun
npm uninstall react-test-renderer @types/react-test-renderer
npm install -D [email protected]

The commands above use [email protected] for React 19.2. Use [email protected] for React 19.1, or [email protected] for React 19.0.

2. Update type imports (if needed)

If you were directly importing types from React Test Renderer, you may need to update your imports:

// Before (v13)
import type { ReactTestInstance } from 'react-test-renderer';

// After (v14)
import type { TestInstance } from 'test-renderer';

Note: Most users won't need to update type imports, as React Native Testing Library now exports the necessary types directly.

See the Test Renderer documentation for more.

Async APIs by Default

With React 18 support dropped, RNTL v14 uses React 19's async rendering model. The following functions are now async by default:

  • render() → returns Promise<RenderResult>
  • rerender() and unmount() → return Promise<void>
  • renderHook() → returns Promise<RenderHookResult>
  • fireEvent() and helpers (press, changeText, scroll) → return Promise<void>
  • act() → always returns Promise<T>
Already using async APIs?

If you adopted the async APIs introduced in RNTL v13.3 (renderAsync, fireEventAsync, renderHookAsync), rename them to their non-async counterparts (render, fireEvent, renderHook). The async versions have been removed since the standard APIs are now async by default.

render is now async

In v14, render is async by default and returns a Promise. This allows proper support for Suspense boundaries and the use() hook.

Before (v13):

import { render, screen } from '@testing-library/react-native';

it('should render component', () => {
  render(<MyComponent />);
  expect(screen.getByText('Hello')).toBeOnTheScreen();
});

After (v14):

import { render, screen } from '@testing-library/react-native';

it('should render component', async () => {
  await render(<MyComponent />);
  expect(screen.getByText('Hello')).toBeOnTheScreen();
});

See the render API documentation.

renderHook is now async

In v14, renderHook is async by default and returns a Promise.

Before (v13):

import { renderHook } from '@testing-library/react-native';

it('should test hook', () => {
  const { result, rerender } = renderHook(() => useMyHook());

  rerender(newProps);
  unmount();
});

After (v14):

import { renderHook } from '@testing-library/react-native';

it('should test hook', async () => {
  const { result, rerender } = await renderHook(() => useMyHook());

  await rerender(newProps);
  await unmount();
});

See the renderHook API documentation.

fireEvent is now async

In v14, fireEvent and its helpers (press, changeText, scroll) are async by default and return a Promise.

Before (v13):

import { fireEvent, screen } from '@testing-library/react-native';

it('should press button', () => {
  render(<MyComponent />);
  fireEvent.press(screen.getByText('Press me'));
  expect(onPress).toHaveBeenCalled();
});

After (v14):

import { fireEvent, screen } from '@testing-library/react-native';

it('should press button', async () => {
  await render(<MyComponent />);
  await fireEvent.press(screen.getByText('Press me'));
  expect(onPress).toHaveBeenCalled();
});

fireEvent.press() and fireEvent.scroll() now create default synthetic native event objects. If you pass event props, they are deep-merged into the default event object:

await fireEvent.press(screen.getByText('Press me'), {
  nativeEvent: { pageX: 20, pageY: 30 },
});

expect(onPress).toHaveBeenCalledWith(
  expect.objectContaining({
    nativeEvent: expect.objectContaining({ pageX: 20, pageY: 30 }),
  }),
);

If your v13 assertions expected the handler to receive exactly the object you passed to fireEvent.press() or fireEvent.scroll(), update them to use partial matching.

act is now async

In v14, act is async by default and always returns a Promise. You should always await the result of act().

What changed:

  • act now always returns Promise<T> instead of T | Thenable<T>
  • act should always be awaited
Note

The transition to async act may prevent testing very short transient states, as awaiting act will flush all pending updates before returning.

Before (v13):

import { act } from '@testing-library/react-native';

it('should update state', () => {
  act(() => {
    setState('new value');
  });
  expect(state).toBe('new value');
});

After (v14):

import { act } from '@testing-library/react-native';

it('should update state', async () => {
  await act(() => {
    setState('new value');
  });
  expect(state).toBe('new value');
});

Note: Even if your callback is synchronous, you should still use await act(...) as act now always returns a Promise.

See the act API documentation.

Why async APIs?

The async APIs properly handle Suspense boundaries and the use() hook, and ensure all pending React updates complete before assertions run. This matches React 19's async rendering model.

Removed APIs

update alias removed

The update alias for rerender has been removed. Use rerender instead:

// Before (v13)
screen.update(<MyComponent />);
const { update } = render(<MyComponent />);
update(<MyComponent newProp />);

// After (v14)
await screen.rerender(<MyComponent />);
const { rerender } = await render(<MyComponent />);
await rerender(<MyComponent newProp />);

getQueriesForElement export removed

The getQueriesForElement export alias for within has been removed. Use within instead:

// Before (v13)
import { getQueriesForElement } from '@testing-library/react-native';

const queries = getQueriesForElement(element);

// After (v14)
import { within } from '@testing-library/react-native';

const queries = within(element);

Note: getQueriesForElement was just an alias for within, so the functionality is identical - only the import needs to change.

UNSAFE_root removed

UNSAFE_root has been removed. Use container to access the pseudo-element container, or root to access the first rendered host element:

// Before (v13)
const unsafeRoot = screen.UNSAFE_root;

// After (v14)
const container = screen.container; // pseudo-element container
const root = screen.root; // first rendered host element

Legacy UNSAFE_* queries removed

The legacy UNSAFE_getAllByType, UNSAFE_getByType, UNSAFE_getAllByProps, and UNSAFE_getByProps queries have been removed. These queries could return composite (user-defined) components, which is no longer supported with Test Renderer as it only renders host elements.

If you were using these legacy queries, you should refactor your tests to use the standard queries (getByRole, getByText, getByTestId, etc.) which target host elements.

// Before (v13)
const buttons = screen.UNSAFE_getAllByType(Button);
const input = screen.UNSAFE_getByProps({ placeholder: 'Enter text' });

// After (v14)
const buttons = screen.getAllByRole('button');
const input = screen.getByPlaceholderText('Enter text');

concurrentRoot option removed

The concurrentRoot option has been removed from both render options and configure function. In v14, concurrent rendering is always enabled, since it's the standard rendering mode for React 19 and React Native's New Architecture.

// Before (v13)
render(<MyComponent />, { concurrentRoot: true });  // Enable concurrent mode
render(<MyComponent />, { concurrentRoot: false }); // Disable concurrent mode
configure({ concurrentRoot: false });               // Disable globally

// After (v14)
await render(<MyComponent />); // Always uses concurrent rendering

Migration: Remove any concurrentRoot options from your render calls and configure function. If you were setting concurrentRoot: true, just remove the option. If you were setting concurrentRoot: false to disable concurrent rendering, this is no longer supported in v14.

createNodeMock option removed

The createNodeMock render option has been removed. It was previously passed through to React Test Renderer, but the v14 Test Renderer integration does not support this option.

// Before (v13)
render(<MyComponent />, {
  createNodeMock: (element) => {
    if (element.type === TextInput) {
      return { focus: jest.fn() };
    }
    return {};
  },
});

// After (v14)
await render(<MyComponent />);

If you used createNodeMock to mock imperative refs, prefer testing user-visible behavior and mock the component or native module at the boundary where the ref behavior is introduced.

container API reintroduced

In v14, the container API has been reintroduced and is now safe to use. Previously, container was renamed to UNSAFE_root in v12 due to behavioral differences from React Testing Library's container. Now container returns a pseudo-element container whose children are the elements you rendered, consistent with React Testing Library's behavior.

What changed:

  • screen.container is now available and safe to use
  • container returns a pseudo-element container from Test Renderer
  • The container's children are the elements you rendered
  • UNSAFE_root has been removed

Before (v13):

import { render, screen } from '@testing-library/react-native';

it('should access root', () => {
  render(<MyComponent />);
  // UNSAFE_root was the only way to access the container
  const root = screen.UNSAFE_root;
});

After (v14):

import { render, screen } from '@testing-library/react-native';

it('should access container', async () => {
  await render(<MyComponent />);
  // container is now safe and available
  const container = screen.container;
  // root is the first child of container
  const root = screen.root;
});

See the screen API documentation.

Text string validation enforced by default

In v14, Test Renderer enforces React Native's requirement that text strings must be rendered within a <Text> component. The unstable_validateStringsRenderedWithinText option has been removed from RenderOptions since this validation is now always on.

What changed:

  • Text string validation is now always enabled and cannot be disabled
  • The unstable_validateStringsRenderedWithinText option has been removed
  • Tests will now throw Invariant Violation: Text strings must be rendered within a <Text> component errors when attempting to render strings outside of <Text> components, matching React Native's runtime behavior

Migration:

If you were using unstable_validateStringsRenderedWithinText: true in your render options, you can simply remove this option as the validation is now always enabled:

// Before (v13)
render(<MyComponent />, {
  unstable_validateStringsRenderedWithinText: true,
});

// After (v14)
await render(<MyComponent />);
// Validation is now always enabled

If you were relying on the previous behavior where strings could be rendered outside of <Text> components, you'll need to fix your components to wrap strings in <Text> components, as this matches React Native's actual runtime behavior.

Hidden Suspense and Activity content

When React keeps previously rendered content hidden, such as suspended content or React 19.2 <Activity mode="hidden">, RNTL now applies React Native-like hidden props to the affected instances. In practice, hidden instances receive display: 'none', preserving existing styles by appending the hidden style.

This makes visibility queries and matchers behave closer to runtime behavior:

  • default queries do not match hidden instances
  • queries with { includeHiddenElements: true } can still find hidden instances
  • toBeVisible() treats these instances as hidden

Accessible name changes

Accessible name calculation has been updated to better match React Native behavior:

  • aria-labelledby and accessibilityLabelledBy still take precedence over explicit labels
  • aria-label and accessibilityLabel take precedence over text content
  • Image alt is used as an accessible label
  • a root TextInput can use its placeholder as its accessible name
  • child accessible names are concatenated with spaces
  • child TextInput placeholders are not used when computing a parent's accessible name

This affects getByRole(..., { name }) and toHaveAccessibleName(). If a v13 role query matched because descendant text or label queries were used as a fallback, update the test to match the element's computed accessible name directly or query the descendant element instead.

Unknown options now warn

RNTL now logs a warning when unknown options are passed to configure, render, renderHook, or userEvent.setup. This helps catch stale v13 options and misspellings during migration.

For example, after removing concurrentRoot, createNodeMock, or unstable_validateStringsRenderedWithinText, make sure those options are not still being passed through shared test helpers.

Codemods

Two codemods are available to automate the migration. Both are safe to run multiple times - they only transform code that hasn't been migrated yet.

rntl-v14-update-deps

Updates your package.json:

  • Removes React Test Renderer (react-test-renderer and @types/react-test-renderer)
  • Adds Test Renderer (test-renderer) using the current recommended 1.x line
  • Updates @testing-library/react-native to the v14 version
npm
yarn
pnpm
bun
npx codemod@latest rntl-v14-update-deps --target .
npm install

rntl-v14-async-functions

Transforms test files:

  • Adds await to render(), act(), renderHook(), fireEvent() calls
  • Makes test functions async when needed
  • Handles screen.rerender(), screen.unmount(), and renderer methods
npm
yarn
pnpm
bun
npx codemod@latest rntl-v14-async-functions --target ./src

Custom render functions

If you have custom render helpers (like renderWithProviders), you can specify them using the customRenderFunctions parameter. The codemod will then also transform calls to these functions:

npm
yarn
pnpm
bun
npx codemod@latest rntl-v14-async-functions \
  --target ./src \
  --param customRenderFunctions="renderWithProviders,renderWithTheme"

This will add await to your custom render calls and make the containing test functions async, just like it does for the standard render function.

Limitations

  • Helper functions defined in test files are not transformed by default
  • Namespace imports (import * as RNTL) are not handled

Full Changelog

https://github.com/callstack/react-native-testing-library/compare/v13.3.3...v14.0.0

Need React or React Native expertise you can count on?