import { PackageManagerTabs } from '@rspress/core/theme';

# 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](https://reactjs.org/docs/test-renderer.html) to [Test Renderer](https://github.com/mdjastrzebski/test-renderer)
- Removed deprecated APIs: `update`, `getQueriesForElement`, `UNSAFE_root`, `concurrentRoot` option, `createNodeMock` option
- Reintroduced `container` API, which is now safe to use

> [!INFO] 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**

<PackageManagerTabs
command={{
    npm: 'npx codemod@latest rntl-v14-update-deps --target .\nnpm install',
    yarn: 'yarn dlx codemod@latest rntl-v14-update-deps --target .\nyarn install',
    pnpm: 'pnpm dlx codemod@latest rntl-v14-update-deps --target .\npnpm install',
    bun: 'bunx codemod@latest rntl-v14-update-deps --target .\nbun install',
  }}
/>

**Step 2: Update test code to async**

<PackageManagerTabs
command={{
    npm: 'npx codemod@latest rntl-v14-async-functions --target ./src',
    yarn: 'yarn dlx codemod@latest rntl-v14-async-functions --target ./src',
    pnpm: 'pnpm dlx codemod@latest rntl-v14-async-functions --target ./src',
    bun: 'bunx 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](./quick-start.md#agent-docs-in-the-package) 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 Version | React Version | React Native Version | Node.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](https://github.com/mdjastrzebski/test-renderer) instead of the deprecated [React Test Renderer](https://reactjs.org/docs/test-renderer.html). 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`](https://github.com/mdjastrzebski/test-renderer#test-instance) 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.

#### Recommended Test Renderer versions

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

| React version | Recommended Test Renderer version | Notable React features                            |
| ------------- | --------------------------------- | ------------------------------------------------- |
| `19.2`        | `test-renderer@1.2`               | `<Activity />`, `useEffectEvent`                  |
| `19.1`        | `test-renderer@1.1`               | Owner Stack support, updated `useId()` format     |
| `19.0`        | `test-renderer@1.0`               | 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](https://github.com/mdjastrzebski/test-renderer#react-19-compatibility-lines) for the latest recommendations.

**Migration:**

#### 1. Update dependencies

Run codemod for updating dependencies:

<PackageManagerTabs
command={{
    npm: 'npx codemod@latest rntl-v14-update-deps\nnpm install',
    yarn: 'yarn dlx codemod@latest rntl-v14-update-deps\nyarn install',
    pnpm: 'pnpm dlx codemod@latest rntl-v14-update-deps\npnpm install',
    bun: 'bunx codemod@latest rntl-v14-update-deps\nbun 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:

<PackageManagerTabs
command={{
    npm: 'npm uninstall react-test-renderer @types/react-test-renderer\nnpm install -D test-renderer@1.2',
    yarn: 'yarn remove react-test-renderer @types/react-test-renderer\nyarn add -D test-renderer@1.2',
    pnpm: 'pnpm remove react-test-renderer @types/react-test-renderer\npnpm add -D test-renderer@1.2',
    bun: 'bun remove react-test-renderer @types/react-test-renderer\nbun add -D test-renderer@1.2',
  }}
/>

The commands above use `test-renderer@1.2` for React 19.2. Use `test-renderer@1.1` for React 19.1, or `test-renderer@1.0` 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:

```ts
// 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](https://github.com/mdjastrzebski/test-renderer) 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>`

> [!TIP] 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):**

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

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

**After (v14):**

```ts
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](../api/render.md).

#### `renderHook` is now async

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

**Before (v13):**

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

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

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

**After (v14):**

```ts
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](../api/render-hook.md).

#### `fireEvent` is now async

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

**Before (v13):**

```ts
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):**

```ts
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:

```ts
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):**

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

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

**After (v14):**

```ts
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](../api/other-helpers.md#act).

#### 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:

```ts
// 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:

```ts
// 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:

```ts
// 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](https://github.com/mdjastrzebski/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.

```ts
// 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.

```ts
// 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.

```ts
// 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):**

```ts
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):**

```ts
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](../api/screen.md#container).

### 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:

```ts
// 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

<PackageManagerTabs
command={{
    npm: 'npx codemod@latest rntl-v14-update-deps --target .\nnpm install',
    yarn: 'yarn dlx codemod@latest rntl-v14-update-deps --target .\nyarn install',
    pnpm: 'pnpm dlx codemod@latest rntl-v14-update-deps --target .\npnpm install',
    bun: 'bunx codemod@latest rntl-v14-update-deps --target .\nbun 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

<PackageManagerTabs
command={{
    npm: 'npx codemod@latest rntl-v14-async-functions --target ./src',
    yarn: 'yarn dlx codemod@latest rntl-v14-async-functions --target ./src',
    pnpm: 'pnpm dlx codemod@latest rntl-v14-async-functions --target ./src',
    bun: 'bunx 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:

<PackageManagerTabs
command={{
    npm: 'npx codemod@latest rntl-v14-async-functions \\\n  --target ./src \\\n  --param customRenderFunctions="renderWithProviders,renderWithTheme"',
    yarn: 'yarn dlx codemod@latest rntl-v14-async-functions \\\n  --target ./src \\\n  --param customRenderFunctions="renderWithProviders,renderWithTheme"',
    pnpm: 'pnpm dlx codemod@latest rntl-v14-async-functions \\\n  --target ./src \\\n  --param customRenderFunctions="renderWithProviders,renderWithTheme"',
    bun: 'bunx codemod@latest rntl-v14-async-functions \\\n  --target ./src \\\n  --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
