skip to content

In React Native Testing Library, how does userEvent.press differ from fireEvent.press, and when is fireEvent still the right tool?

level: middleimportance: must knowfreq 52%

answer

  1. one handler versus a whole sequence
  2. pressIn, pressOut, then press
  3. host elements only for userEvent
  4. fireEvent walks up, composites included
  5. at least 130 ms per press

basics

~20 s

fireEvent.press finds the nearest enabled onPress handler, walking up through composite components, and calls it once. userEvent.press replays React Native's press sequence on host elements, pressIn, pressOut, then press, in at least 130 ms. Keep fireEvent for events userEvent lacks.

solid answer

~40 s

In React Native Testing Library, `fireEvent.press(element)` builds a press event, then walks up from the element to the first enabled `onPress` handler, on host or composite components, and calls it inside `act`; nothing else fires. `userEvent.press(element)` from `userEvent.setup()` simulates the interaction the way React Native's runtime delivers it, on host elements only: for a `Pressable` it drives the responder events, so `onPressIn`, `onPressOut` and `onPress` fire in order and `disabled` is honoured. It waits at least 130 ms, React Native's minimum press duration, and `longPress` defaults to 500 ms. I default to `userEvent` for presses, typing and scrolling, and keep `fireEvent` for events it does not model, such as `layout`, `blur` or a custom `onXxx` callback on a composite component.

code

tsx · 34 lines
tsx
import { useState } from 'react';
import { Pressable, Text } from 'react-native';
import { fireEvent, render, screen, userEvent } from '@testing-library/react-native';

function GenreChip({ label, onSelect }: { label: string; onSelect: () => void }) {
  const [pressed, setPressed] = useState(false);
  return (
    <Pressable
      role="button"
      onPressIn={() => setPressed(true)}
      onPressOut={() => setPressed(false)}
      onPress={onSelect}
    >
      <Text>{pressed ? `${label} (pressed)` : label}</Text>
    </Pressable>
  );
}

test('userEvent runs the full press sequence', async () => {
  const onSelect = jest.fn();
  await render(<GenreChip label="Drama" onSelect={onSelect} />);
  const user = userEvent.setup();

  await user.press(screen.getByRole('button', { name: 'Drama' }));
  expect(onSelect).toHaveBeenCalledTimes(1);
});

test('fireEvent calls onPress only', async () => {
  const onSelect = jest.fn();
  await render(<GenreChip label="Drama" onSelect={onSelect} />);

  await fireEvent.press(screen.getByRole('button', { name: 'Drama' }));
  expect(onSelect).toHaveBeenCalledTimes(1); // onPressIn and onPressOut never ran
});

go deeper

for a junior

Recall that userEvent simulates a real press with several events, fireEvent calls one handler, and both must be awaited in v14.

for a middle

Explain the press sequence, the host-only rule, fireEvent's walk up through composite handlers and its silent no-op when nothing is found.

for a senior

Choose per interaction: userEvent by default for fidelity, fireEvent for unmodelled events and composite callbacks, and account for the 130 ms press cost in large suites.

for a principal

Set suite conventions that balance fidelity and speed, such as userEvent by default, fake timers for press-heavy suites and lint rules against un-awaited events.

## Two APIs, two levels of realism React Native Testing Library ships two ways to trigger interactions. **`fireEvent`** is the original API. `fireEvent(element, 'press', ...data)` or the helper `fireEvent.press(element)`: 1. starts at the given host element; 2. looks for a handler named `onPress` in its props and, through React's internal tree, in the composite components that rendered it; 3. if none is found, or the element is disabled, moves up to the parent and repeats; 4. calls the handler it finds, inside an async `act`, with the event object you pass (the `press` and `scroll` helpers build a default one and merge yours into it); 5. if no handler exists anywhere up the tree, **returns silently**. **`userEvent`** simulates what a user does. `const user = userEvent.setup()` creates an instance, and `await user.press(element)` replays the event sequence React Native emits: - for elements with direct press handlers, such as a host `Text` with `onPress`: `pressIn`, then `pressOut`, then `press`; - for `Pressable` and touchables: the responder grant and release events, which React Native's pressability logic turns into `onPressIn`, `onPressOut` and `onPress`, honouring `disabled`; - events are dispatched **only on host elements**, each with a realistic event object. ## Side by side | | `fireEvent.press` | `userEvent.press` | |---|---|---| | What fires | One `onPress` handler | The full press sequence | | Where it looks | Host and composite handlers, walking up | Host elements only | | Event object | Default touch event plus your overrides | Built automatically for each event | | Missing handler | Silently does nothing | Walks up to the nearest pressable, or does nothing | | Duration | Immediate | At least 130 ms; `longPress` 500 ms by default | | Return | `Promise<void>` in v14 | `Promise<void>` | The 130 ms comes from React Native's pressability minimum press duration. With real timers every press adds real time to the test; tests that press a lot usually run with fake timers, which the default `userEvent.setup()` detects and advances. ## Why userEvent catches more bugs In a movie app, a filter chip is a `Pressable` whose `onPressIn` highlights the chip and whose `onPress` applies the filter. - `fireEvent.press(chip)` calls `onPress` only, so a bug in `onPressIn` stays hidden. - `user.press(chip)` runs the sequence, so the highlight, the `disabled` handling and the ordering all execute as on a device. - `user.longPress(chip, { duration: 800 })` fits a chip that opens options on long press with a custom `delayLongPress`. ## When fireEvent is still right - **Events userEvent does not model**: `fireEvent(element, 'layout', layoutEvent)`, `blur`, `focus` alone, `contentSizeChange`. - **Handlers on composite components**: a custom `MovieCard` exposing `onFavorite` that no host element receives can be triggered with `fireEvent(card, 'favorite')`. - **Speed in very large suites** where the press sequence adds nothing, accepting the lower fidelity. ## Pitfalls - A `fireEvent.press` on an element with no handler passes silently, so assert on the outcome, not merely that the call did not throw. - Both APIs are async in v14: missing `await` lets assertions run before the handler.

  • What happens when you fireEvent.press a Text that has no onPress anywhere above it?
    Nothing, and no error. `fireEvent` walks up the tree looking for an enabled handler and returns silently if it finds none. A test that only calls `fireEvent.press` without asserting an outcome can therefore pass while the button is not wired at all.
  • Does userEvent.press respect a disabled Pressable?
    Yes. For a `Pressable` it goes through the responder events, and the pressability logic declines the gesture when `disabled` is set, so no `onPress` fires. `fireEvent` also skips disabled elements, but it gets there by checking responder props, not by replaying the gesture.

fireEvent is like calling a shop's order line directly; userEvent is walking in, lifting the item and paying at the till, and the till only rings if every step works.

saying these in an interview costs you the question

  • fireEvent.press triggers onPressIn and onPressOut too.
  • userEvent.press calls handlers on composite components directly.
  • fireEvent.press throws when the element has no onPress handler.
  • userEvent.press completes instantly, like a function call.
  • fireEvent is deprecated and should never be used in RNTL 14.