TIL: fireEvent Is Not the Same as userEvent in React Testing Library
I used to treat fireEvent and userEvent as two ways to click or type in a test. They aren't the same thing. One fires an event. The other tries to describe what a user actually does.
I used to write React Testing Library tests like this without thinking too much about it:
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
It worked. The test passed. Everyone moved on.
Then I hit a form test that felt a bit too easy. The component had validation, disabled states, focus handling, and a small keyboard shortcut. My test was still passing, but when I tried the same flow in the browser, the behavior wasn't exactly the same. That was the moment I had to slow down and look again at something I had mostly ignored: the difference between fireEvent and userEvent.
Small detail. Big effect.
fireEvent is the lower-level tool. It tells the DOM, "this event happened". If you call fireEvent.click(button), you're firing a click event on that button. That's useful, and sometimes it's exactly what you want.
userEvent works from a different angle. It tries to describe the action a real user would take. A user doesn't usually fire a single event. They click, focus, type, tab, select, paste, and trigger a chain of browser behavior around those actions. So when we write await user.click(button) or await user.type(input, 'hello'), we're closer to the way the UI is used in real life.
That's the reason I now reach for userEvent first.
The quick mental model
The easiest way I remember it is this:
fireEvent is about events.
userEvent is about behavior.
That sounds like a small wording change, but it changes how I write tests. With fireEvent, I often start thinking like a developer. Which event should I trigger? Is it change, input, keydown, keyup, or click?
With userEvent, I start closer to the user flow. Click this field. Type this text. Press Enter. Tab to the next control. Submit the form.
That usually gives me a better test.
A simple example
Let's say we have this small search form:
import { useState } from 'react';
type SearchBoxProps = {
onSearch: (value: string) => void;
};
export function SearchBox({ onSearch }: SearchBoxProps) {
const [value, setValue] = useState('');
return (
<form
onSubmit={(event) => {
event.preventDefault();
onSearch(value);
}}
>
<label htmlFor="search">Search</label>
<input
id="search"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
<button type="submit">Search</button>
</form>
);
}
A test with fireEvent could look like this:
import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { SearchBox } from './SearchBox';
test('submits the search value', () => {
const onSearch = vi.fn();
render(<SearchBox onSearch={onSearch} />);
fireEvent.change(screen.getByLabelText(/search/i), {
target: { value: 'react testing library' },
});
fireEvent.click(screen.getByRole('button', { name: /search/i }));
expect(onSearch).toHaveBeenCalledWith('react testing library');
});
This isn't a bad test. It says what we need, and in many cases it will be enough.
But it's not quite how a person uses the form. A person clicks or focuses the input, types each character, and then submits. So I would usually write it like this now:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { SearchBox } from './SearchBox';
test('submits the search value', async () => {
const user = userEvent.setup();
const onSearch = vi.fn();
render(<SearchBox onSearch={onSearch} />);
await user.type(
screen.getByLabelText(/search/i),
'react testing library'
);
await user.click(screen.getByRole('button', { name: /search/i }));
expect(onSearch).toHaveBeenCalledWith('react testing library');
});
There are two small changes here.
First, the test is async, because most userEvent actions should be awaited. Second, we're not manually setting the input value through an event payload. We're asking the test to type into the field.
It reads better too. That matters more than we admit.
Why userEvent.setup() is worth using
Older examples often show calls like this:
await userEvent.click(button);
That still exists, but I prefer the setup style:
const user = userEvent.setup();
Then the test uses that user instance:
await user.click(button);
await user.type(input, 'hello');
The setup approach makes the test feel like one browser session. It also keeps the door open for more complex flows, like keyboard state, clipboard behavior, or fake timer setup.
I also like that it creates a small ritual at the top of each interaction test. If I see const user = userEvent.setup(), I immediately know this test is going to simulate user behavior, not just poke the DOM.
So, should we stop using fireEvent?
No. I don't think that's the right takeaway.
fireEvent is still useful when you need to trigger a specific event and userEvent doesn't cover the interaction well. Some browser events are awkward to express as a normal user action. Drag and drop, animation events, media events, resize events, and a few custom cases can still be clearer with fireEvent.
For example:
fireEvent.animationEnd(element);
fireEvent.drop(dropZone, {
dataTransfer: {
files: [file],
},
});
In those cases, I don't want to force userEvent into a shape that doesn't fit. A test can be user-focused and still use fireEvent where the lower-level event is the honest thing to test.
The mistake is using fireEvent by default for every button click and every form input. That's where tests can become too mechanical.
A small bug this can catch
Imagine a submit button that should be disabled until the user fills in a field:
<button disabled={!value}>Search</button>
With fireEvent, it's easy to accidentally write a test that clicks something without caring enough about whether a user could actually click it in the browser.
With userEvent, you get a better signal. It checks more of the real interaction path, including whether an element is visible or disabled in cases where that matters. So if the button can't be clicked by a real user, the test is less likely to pretend everything is fine.
That doesn't mean userEvent is magic. It won't replace thinking. It just moves the test a bit closer to the product.
My rule of thumb
For most component tests, I now use this rule:
Use userEvent when the test describes something a user does.
Use fireEvent when the test needs a specific DOM event.
That means clicks, typing, tabbing, selecting options, clearing inputs, uploading files, and keyboard flows usually start with userEvent.
Specific events, custom event payloads, or odd browser edge cases can use fireEvent.
It's not about making every test longer. Actually, the opposite often happens. A good userEvent test tends to read like a tiny user story:
const user = userEvent.setup();
render(<SearchBox onSearch={onSearch} />);
await user.type(screen.getByLabelText(/search/i), 'vue');
await user.click(screen.getByRole('button', { name: /search/i }));
expect(onSearch).toHaveBeenCalledWith('vue');
That's easy to scan during a code review. You don't need to reverse-engineer event payloads in your head.
The part I wish I had learned earlier
React Testing Library pushes us to test the UI from the user's point of view. Queries like getByRole and getByLabelText already guide us in that direction. userEvent is the same idea applied to interactions.
The test should not care that the component uses useState, React Hook Form, Formik, or a custom reducer. It should care that a user can fill in the field and submit the form.
That's the real benefit.
fireEvent isn't wrong. It's just closer to the DOM than to the user. Once I started seeing that, the choice became much easier.