Absolutely everything you need to know about act() in React tests

September 20, 2025
#testing#react-testing-library

When writing React tests, you will quickly become familiar with the act() function. Despite being a fundamental concept to testing your React apps, it is often one of the most confusing and misunderstood aspects of testing React applications.

I've also in the past found it hard to articulate why we need it to engineers learning how to test their React apps. (But hopefully I clear it up for you in this page!).

What is act(), why do we need it and when should you use it?

In your tests, functionality that updates internal state of a rendered React component should be wrapped in act() so we can be sure that all state changes and side effects have been fully processed by React, before the rest of your test (i.e. assertions) continues.

This makes sure that your tests are testing things in a realistic way (without it, you might be making assertions on 'old' state values, without realising).

If you use React Testing Library (RTL) functions like the following, you don't have to think about act() as they already wrap their functionality in act():

  • user interaction with userEvent (like await userEvent.click(...))
  • the await findBy... functions like await screen.findByText("...")
  • the waitFor(...) function

But - there will definitely be times you will see the famous update was not wrapped in act() error that will plague your command line output if you are not careful.

Understanding the "update was not wrapped in act(...)" error

You have probably seen this a few times in your test result output:

An update to %s inside a test was not wrapped in act(...).

When testing, code that causes React state updates
should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This is warning you that your tests are not wrapped in act(). This means you might be making assertions in your test against 'old' state and you might not notice or catch real bugs.

(I've definitely ignored the warnings in the past, and introduced big bugs that were so easy to catch if I had used act() correctly!).

To see some ways to get rid of the error, see the section below

Now, let's look at it in depth

The above is really all you need to know about React's act(). But if you are reading this blog, you are probably interested in really understanding everything about it. It is a very important part of writing tests, so it makes sense to fully understand it.

Which act() should you import (React vs Testing Library)?

Firstly to clear up some confusion: React itself includes act. You can import React, { act } from 'react'.

But you should never do this. Always import from @testing-library/react:

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

This versions from @testing-library/react is a very lightweight wrapper without much extra functionality. It just wraps around the actual act() implementation from react, but with these changes:

  • ensures some environment configuration is in place.
    • React will warn you if you try to call act() outside of a test environment (when IS_REACT_ACT_ENVIRONMENT is not set to true).
    • Using this act() wrapper from @testing-library ensures that is set up correctly.
  • it also adds better compatibility if you are running an older version of React, although in 2025 it is unlikely you will need this. I won't go into detail here, as this won't affect many people nowadays.

If you are interested, you can see how RTL's act() is implemented on GitHub (and you might notice there is a third version of act(), for legacy reasons).

It was more of an issue years ago, and in 2025 you can often get away with using act() from the react package, but for consistency and those very rare edge cases it is better to always import from the RTL one!

An example

In the following example, I will show when forgetting to use act() will mean your current rendered state in the test is outdated.

Let's test this simple component, it will automatically update the count by 1 after 10ms:

const AutoCounter = () => {
  const [count, setCount] = useState(0);

  // automatically increment after 10ms

  useEffect(() => {
    setTimeout(() => {
      setCount(c => c + 1);
    }, 10);
  }, []);

  return <h1>Count: {count}</h1>;
};

If we write the following test, which uses fake timers, it will fail, as the rendered component is still showing Count: 0:

vi.useFakeTimers();

test('auto-increments after 10ms', () => {
  render(<AutoCounter />);
  const heading =
    screen.getByRole('heading');

  expect(heading).toHaveTextContent(
    'Count: 0'
  );

  // advancing fake timers by 10ms would
  // trigger the setTimeout in the component
  // which itself will trigger a state change
  vi.advanceTimersByTime(10);
  // note: you can also run only pending timers: vi.runOnlyPendingTimers()

  expect(heading).toHaveTextContent(
    'Count: 1'
  ); // ❌ fails
});

But if you wrap the vi.advanceTimersByTime(10) in act(...), it will now pass:

vi.useFakeTimers();

test('auto-increments after 10ms', async () => {
  render(<AutoCounter />);
  const heading =
    screen.getByRole('heading');

  expect(heading).toHaveTextContent(
    'Count: 0'
  );

  // trigger state change when timers advance to 10ms
  // via the setTimeout. But this time, wrapped in act()
  // so all state changes are synced before we do our
  // assertions
  await act(async () => {
    vi.advanceTimersByTime(10);
  });

  expect(heading).toHaveTextContent(
    'Count: 1'
  );
});

In a real test, you could also just await screen.findByText('Count: 1') too!

Because the await findBy... functions use act() internally

Another example, this time calling the click functionality directly on a HTMLButton (not via userEvent)

const Counter = () => {
  const [count, setCount] = useState(0);

  const onClick = () =>
    setCount(prev => prev + 1);
  return (
    <div>
      <button onClick={onClick}>
        Increment
      </button>
      <h1>Count: {count}</h1>
    </div>
  );
};

test('clicking button updates count', async () => {
  render(<Counter />);

  const button =
    screen.getByRole('button');
  const heading =
    screen.getByRole('heading');

  expect(heading).toHaveTextContent(
    'Count: 0'
  );

  // Calling click() directly on DOM element - this needs act()!
  await act(async () => {
    button.click();
  });

  expect(heading).toHaveTextContent(
    'Count: 1'
  ); // ✅ Passes!
  // but this would have failed if `act()` was not used, as it would currently
  // still be `Count: 0`
});

Using act() when testing hooks

I find this is one of the most common ways where you have to use act().

If you are testing a hook in isolation with renderHook(), you might be calling functions which update state. In those cases you have to wrap it in act()

Example of using act() in a hook test:

This is a simple hook we want to test:

const useCounter = () => {
  const [value, setValue] = useState(0);
  const increment = () => {
    setValue(val => val + 1);
  };
  return {
    increment,
    value,
  };
};

The following test will fail as the value of result.current.value remains at 0

test('can increment the counter', async () => {
  const { result } =
    renderHook(useCounter);

  expect(result.current.value).toBe(0); // ✅ passes

  result.current.increment();

  expect(result.current.value).toBe(1); // ❌ fails!

  result.current.increment();
  expect(result.current.value).toBe(2); // ❌ fails!
});

But using our friend act(), it will now have correct state value

test('can increment the counter', async () => {
  const { result } =
    renderHook(useCounter);

  expect(result.current.value).toBe(0); // ✅ passes

  await act(async () => {
    result.current.increment();
  });

  expect(result.current.value).toBe(1); // ✅ passes

  await act(async () => {
    result.current.increment();
  });
  expect(result.current.value).toBe(2); // ✅ passes
});

When are times that you may have to use act()?

  • When testing hooks & you are making state changes
  • If you are manually triggering state change, for example calling someButtonElement.click() directly
  • When manually dispatching events (not via userEvent)
  • Sometimes when triggering events (like clicks) via fireEvent - especially when it triggers some async functionality
  • When state change happens in timers, like setTimeout, setInterval
  • When using fake timers in Jest/Vitest
  • When async promises resolve and result in state changes

As explained elsewhere in this post, you should normally try to wait for render/state changes (with waitFor or findBy...) than over use act().

A note on fireEvent

In most cases, you don't need to wrap your fireEvent calls (such as fireEvent.click(...)) in act().

However there are times when you will have to, especially if fireEvent triggers an async function which itself has some await inside of it.

This might be a bit controversial, but in my experience, I tend to skip act() for fireEvent, unless the warning shows up in the terminal or it isn't working as I expect. (Note: it is better if you can to just use userEvent functionality to trigger things like clicks if possible anyway.)

Testing async behaviour

Here is a contrived example (realistically, you can avoid act() and just use await screen.findBy...).

import {
  render,
  screen,
  act,
} from '@testing-library/react';
import { useState } from 'react';

const AsyncButton = () => {
  const [status, setStatus] =
    useState('idle');

  const handleClick = async () => {
    setStatus('loading');
    // Simulate an API call
    await new Promise(resolve =>
      setTimeout(resolve, 100)
    );
    setStatus('done');
  };

  return (
    <div>
      <button onClick={handleClick}>
        Click me
      </button>
      <p>Status: {status}</p>
    </div>
  );
};

test('button updates status after async operation', async () => {
  render(<AsyncButton />);

  const button =
    screen.getByRole('button');

  expect(
    screen.getByText('Status: idle')
  ).toBeInTheDocument();

  // Without act(), the async state updates won't be reflected
  await act(async () => {
    button.click();
    // Wait for the async operation to complete
    await new Promise(resolve =>
      setTimeout(resolve, 100)
    );
  });

  expect(
    screen.getByText('Status: done')
  ).toBeInTheDocument();
});

(like many of these examples, you can also just await screen.findByText("Status: done") to avoid act() too)

Async vs sync behaviour of act()

In almost all cases where inside your act() call you are not calling async functions you can normally do it without async/await. But - there are some edge cases where due to how React updates it won't always work.

So you should just default to always await act(async () => {...}).

Note: there are plans to deprecate the non async version.

Here is the official React docs describing it, which I will copy/paste in as I don't think I can explain it in a more succinct way:

async actFn: An async function wrapping renders or interactions for components being tested. Any updates triggered within the actFn, are added to an internal act queue, which are then flushed together to process and apply any changes to the DOM. Since it is async, React will also run any code that crosses an async boundary, and flush any updates scheduled.

You should always use async when:

  • The code inside act() contains promises, async/await, or timers
  • If you are not sure if you need to - because async is always safe

You can use the sync version when:

  • Simple synchronous state updates only
  • But async is still recommended for consistency

(Most of the examples in this page would work with just act(() => {...}) btw. But it is easier to just always use async/await)

When NOT to use act()

Using act() should be a last resort. For most code you don't need it.

Here is when not to use it:

Do not wrap React Testing Library functions in act()

As mentioned elsewhere in this post, you shouldn't wrap things like userEvent.click() in act().

(They do it internally anyway).

// ❌ do not do this:
await act(async () => {
  await userEvent.click(button);
});

// ✅ you can just await the click() instead:
await userEvent.click(button);

Avoid act() if you can just await waitFor() or await findBy...

It is very common to be able to avoid calling act() by just using the waitFor or any of the findBy... functions which will run async code within act, until their assertions pass.

For example, the following can be replaced with a .findBy call

await act(async () => {
  someElement.click();
});

expect(
  screen.getByText(
    'you clicked something'
  )
).toBeInTheDocument();

The following (manually triggering click(), then await findByText()) will almost definitely work too, without any warning from React saying a state change was not wrapped in act():

someElement.click();

// replaced with await findByText():
expect(
  await screen.findByText(
    'you clicked something'
  )
).toBeInTheDocument();

Another example where we can replace act() with a waitFor:

await act(async () => {
  vi.runOnlyPendingTimers();
});

expect(
  screen.getByText('some updated text')
).toBeInTheDocument();

Can be replaced with:

vi.runOnlyPendingTimers();

await waitFor(() => {
  expect(
    screen.getByText(
      'some updated text'
    )
  ).toBeInTheDocument();
});

Why is it called act?

In the testing world, there is a concept of:

  • arrange (set up the world, ready for your test)
  • act (run the code that you are testing)
  • assert (make assertions to check the unit under test works correctly)

Performance issues when over using act()

When you use React Testing Library you cannot avoid using act() as it is used in many of the core functionality that you need to use. But overusing (e.g. wrapping everything in an unnecessary act()) can end up affecting how long your tests take to run.

Tips/best practices

  • remember to always use the act() imported from @testing-library/react, not the one imported from the react package.
  • always use the async version - await act(async () => ...)
  • don't wrap everything in act(). Try to use it as infrequently as possible, and wrap very small chunks (ideally just 1 line/function call)

Debugging React Testing Library "act(...)" Warnings

If you get that warning about act, or some state change is not reflected in your tests, here are the steps I normally take to debug it:

First, find the code/function in your component that is triggering the warning:

  • Add .only to one test: test.only('my test', ...) to find one test that has a act() warning
  • Add early return statements after each step to narrow down where the warning occurs
    • Can't find it? Put return right after render()
    • If warning appears: the component has automatic state updates (likely async)

Now you know where / what is causing it, use the following tips & ideas to fix the issue:

Wrap manual state changes from your tests in act()

In your test, wrap any of your code that triggers state changes in act:

await act(async () => {
  // your state-changing code here
});

Wait for (or findBy...) DOM updates

In your test, use the .findBy...() (or waitFor()) functions to wait until an assertion passes. This uses act() internally, so should make the warning go away.

await screen.findByText(
  'expected text'
);
await waitFor(() =>
  expect(
    someMockFunction
  ).toHaveBeenCalled()
);

Still stuck?

  • Missing await? Add await before any act() calls
  • Double wrapping? Check you're not wrapping act() inside another act()
  • Third-party components? Dropdowns, modals often cause issues
  • Test cleanup? Check beforeEach/afterEach hooks clear everything
  • useEffect cleanup? Check the cleanup function (returned in the useEffect) in components
  • End-of-test flush? Add await act(() => {}) at test end to make sure act() runs when any state changes at the end of the test

How to fix Warning: The current testing environment is not configured to support act(...) warning in test terminal output?

This error comes up because when you call act(), it will check that global.IS_REACT_ACT_ENVIRONMENT is equal to true

If you use React Testing Library (which I highly recommend you do - this entire site HowToTestFrontend.com is assuming you use it), then you shouldn't have to set this

But if you do, in your Vitest or Jest setup file, ensure you set:

global.IS_REACT_ACT_ENVIRONMENT = true;

Found this useful? Share this article

TwitterLinkedIn Facebook

Want to become a pro at testing your React apps?

I've got a huge range of interactive lessons from beginner to expert level.

Get better at testing every week:

Join developers, engineers and testers who receive my free newsletter with React testing tips

I've been writing tests for years, but as I research content for this blog, I am always discovering new things in the tools we use every day!

Get free React/testing tips 👇