When writing tests with React Testing Library, you eventually need to simulate how someone interacts with the UI. The library gives you two tools for that: the built-in fireEvent and the companion package @testing-library/user-event. They look similar at first glance, but they operate at very different levels.
fireEvent: the low-level dispatcher
fireEvent is a thin wrapper around the browser's dispatchEvent API. It triggers one specific DOM event on one element and stops there.
fireEvent.click(screen.getByRole('button'));
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'hello' },
});
The important word is single. A real click usually triggers a chain such as pointerdown, mousedown, focus, mouseup, and click. fireEvent.click() skips that sequence and dispatches only the final event.
That shortcut can hide bugs. If a component reacts on onInput instead of onChange, a test built around fireEvent.change() may pass while the real UI still breaks.
userEvent: the real simulation
@testing-library/user-event models what the browser actually does when a person types, clicks, tabs, or focuses an element. Typing fires per-character keyboard and input events. Clicking includes the full pointer and mouse chain. Focus management also happens the way a user would expect.
import userEvent from '@testing-library/user-event';
const user = userEvent.setup();
await user.type(
screen.getByLabelText(/email/i),
'hello@example.com'
);
await user.tab();
await user.click(screen.getByRole('button', { name: /submit/i }));
The other practical difference is that userEvent is asynchronous. Each interaction should be awaited so the test stays aligned with the event sequence and state updates it triggers.
When to use which
| Scenario | Recommended tool |
|---|---|
| Typing, clicking, tabbing, and other user actions | userEvent |
| Dispatching a custom CustomEvent | fireEvent |
| Triggering events on non-interactive nodes | fireEvent |
| Testing isolated event handler logic | fireEvent |
The rule of thumb is simple: if a real person at a browser could perform the interaction, prefer userEvent. If you need to dispatch a synthetic event that does not map cleanly to a physical action, fireEvent is the right escape hatch.
Bottom line
Default to userEvent. It catches bugs that fireEvent can miss because it exercises the component the way it is actually used.
npm install --save-dev @testing-library/user-event