I still remember the first time I sat puzzled in front of a failing React test. Everything looked right. The button click worked. The API mock was returning data. But for some reason, the element I expected to appear just… didn’t. Or at least, not in time for the test to catch it. That was my introduction to the frustrating yet eye-opening world of asynchronous UI testing—and the hero that eventually saved the day? waitFor
from React Testing Library.
If you’ve ever dealt with dynamic content or asynchronous events in your React apps (and let’s be honest, who hasn’t?), then understanding waitFor
is essential. In this post, I’ll walk you through why you need it, how to use it, and common mistakes I’ve personally made so you can avoid them.
The Problem: Timing Is Everything
Let’s say you’re testing a component that fetches user data from an API after clicking a button. Here’s a simplified version:
function UserProfile() {
const [user, setUser] = React.useState(null);
const fetchUser = async () => {
const res = await fetch('/api/user');
const data = await res.json();
setUser(data);
};
return (
<div>
<button onClick={fetchUser}>Load User</button>
{user && <p>{user.name}</p>}
</div>
);
}
Now let’s test it.
import { render, screen, fireEvent } from '@testing-library/react';
import UserProfile from './UserProfile';
test('loads and displays user', async () => {
render(<UserProfile />);
fireEvent.click(screen.getByText('Load User'));
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
Oops. ❌ This will fail. Why? Because the test checks for the user’s name immediately after the button is clicked, not giving the async fetch enough time to complete. Enter waitFor
.
What is waitFor
?
waitFor
is a utility provided by React Testing Library that waits for the provided callback to stop throwing errors before continuing. It’s perfect for waiting on DOM changes that result from async operations.
Here’s the fixed test:
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
test('loads and displays user', async () => {
render(<UserProfile />);
fireEvent.click(screen.getByText('Load User'));
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
Much better. ✅
When Should You Use waitFor
?
Use waitFor
when:
- You’re waiting for a UI update that happens asynchronously.
- You want to assert on a side effect (e.g., data fetching, state update).
findBy
queries don’t fit the use case.
That said, prefer using findBy
when possible. For example:
await screen.findByText('John Doe');
This is cleaner and more intention-revealing for many scenarios. But waitFor
offers flexibility when your assertions go beyond just querying elements.
A Real-World Example
Imagine you’re testing a table update after a data fetch. Here’s a real use-case we covered in our blog on React Table Design Ideas. Suppose a search feature updates the table:
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
This example shows the power of combining multiple assertions inside one waitFor
. It’s especially useful when a UI change is conditional and time-sensitive.
Key Options You Can Pass
waitFor
accepts an options object with two main parameters:
- timeout: How long to wait before throwing an error (default: 1000ms)
- interval: How frequently to re-run the callback (default: 50ms)
Example:
await waitFor(() => {
expect(screen.getByText('Done')).toBeInTheDocument();
}, { timeout: 2000, interval: 100 });
Customizing these values can help make your tests more reliable—especially in slow environments.
Common Mistakes I’ve Made (So You Don’t Have To)
❌ Using waitFor
with non-throwing callbacks
await waitFor(() => screen.getByText('Success'));
This does nothing because getByText
throws if not found, but you’re not asserting anything. Instead:
await waitFor(() => {
expect(screen.getByText('Success')).toBeInTheDocument();
});
❌ Nesting waitFor
inside act
React Testing Library handles act
internally. No need to do this:
await act(async () => {
await waitFor(...);
});
Just write:
await waitFor(...);
❌ Overusing waitFor
Sometimes a findBy
does the job cleaner:
await screen.findByText('Welcome');
Use waitFor
when you need multiple or complex assertions.
Performance Tips
To make your tests faster and less flaky:
- Avoid unnecessary waits.
- Don’t test implementation details.
- Use mock service workers (MSW) to mock APIs.
- Prefer
findBy
andqueryBy
for better readability.
Bonus: A Custom Utility with waitFor
I often abstract repetitive waits into utilities. For example:
export const waitForLoadingToFinish = () => {
return waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
};
Then in tests:
await waitForLoadingToFinish();
Cleaner. DRY. Reusable. ✅
Learning More
Once you get a grip on waitFor
, you’ll want to expand your React Testing Library skills further. Check out my post on the Top 10 React Testing Library Queries to go even deeper into testing best practices.
If you’re just getting into React or preparing for interviews, I highly recommend trying our React Interview Quiz to challenge your knowledge.
External Resources
Here are a few official and expert-curated links to further boost your testing skills:
Final Thoughts
Debugging flaky tests is one of the most frustrating parts of frontend development. Understanding how and when to use waitFor
will make your tests more reliable, your debugging easier, and your dev life way smoother.
Now that you’ve seen how waitFor
fits into real-world scenarios, it’s time to explore more. Whether it’s optimizing table designs or deep-diving into React concepts, we’re always exploring practical code at Decode Fix.
Want to test your broader dev knowledge too? Try your hand at our Complete SQL Quiz and sharpen your backend skills too. 👨💻🚀