Skip to main content
The act function wraps test code that renders or updates components, ensuring all effects and renders are flushed before continuing.

Signature

function act(callback: () => void | Promise<void>): Promise<void>
callback
() => void | Promise<void>
required
The test code to run. Can be synchronous or return a Promise.
Returns a Promise that resolves when all effects and renders have been flushed.

How it works

From test-utils/src/index.js:27-112:
  1. Calls setupRerender() to enable synchronous rendering
  2. Overrides options.requestAnimationFrame to capture effect callbacks
  3. Executes your callback
  4. Flushes all pending renders
  5. Flushes all pending effects
  6. Repeats until no more work is pending
  7. Calls teardown() to restore options
The function supports nested calls and both sync and async callbacks.

Usage

Basic usage

import { render } from 'preact';
import { act } from 'preact/test-utils';

test('renders component', async () => {
  const container = document.createElement('div');
  
  await act(() => {
    render(<MyComponent />, container);
  });
  
  expect(container.textContent).toBe('Hello World');
});

Testing state updates

import { useState } from 'preact/hooks';
import { render } from 'preact';
import { act } from 'preact/test-utils';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

test('updates on click', async () => {
  const container = document.createElement('div');
  
  await act(() => {
    render(<Counter />, container);
  });
  
  expect(container.querySelector('span').textContent).toBe('0');
  
  await act(() => {
    container.querySelector('button').click();
  });
  
  expect(container.querySelector('span').textContent).toBe('1');
});

Testing effects

import { useEffect, useState } from 'preact/hooks';
import { render } from 'preact';
import { act } from 'preact/test-utils';

function DataLoader({ url }) {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetch(url).then(r => r.json()).then(setData);
  }, [url]);
  
  return <div>{data ? data.title : 'Loading...'}</div>;
}

test('loads data', async () => {
  const container = document.createElement('div');
  
  // Mock fetch
  global.fetch = () => Promise.resolve({
    json: () => Promise.resolve({ title: 'Test' })
  });
  
  await act(async () => {
    render(<DataLoader url="/api/data" />, container);
    await new Promise(r => setTimeout(r, 0));
  });
  
  expect(container.textContent).toBe('Test');
});

Async callbacks

test('handles async updates', async () => {
  const container = document.createElement('div');
  
  await act(async () => {
    render(<MyComponent />, container);
    await someAsyncOperation();
    // More updates...
  });
  
  // All updates are flushed
  expect(container.textContent).toBe('Updated');
});

Implementation details

From test-utils/src/index.js:27-112:
export function act(cb) {
  if (++actDepth > 1) {
    // Nested calls: just execute callback
    try {
      const result = cb();
      if (isThenable(result)) {
        return result.then(
          () => { --actDepth; },
          e => { --actDepth; throw e; }
        );
      }
    } catch (e) {
      --actDepth;
      throw e;
    }
    --actDepth;
    return Promise.resolve();
  }

  const previousRequestAnimationFrame = options.requestAnimationFrame;
  const rerender = setupRerender();
  let flushes = [];

  // Capture effect callbacks
  options.requestAnimationFrame = fc => flushes.push(fc);

  const finish = () => {
    try {
      rerender();
      while (flushes.length) {
        let toFlush = flushes;
        flushes = [];
        toFlush.forEach(x => x());
        rerender();
      }
    } finally {
      teardown();
    }
    options.requestAnimationFrame = previousRequestAnimationFrame;
    --actDepth;
  };

  let result = cb();
  
  if (isThenable(result)) {
    return result.then(finish);
  }
  
  finish();
  return Promise.resolve();
}
The function uses a depth counter to handle nested act() calls. Only the outermost call performs flushing.

Best practices

Always await act

// ✅ Good
await act(() => {
  render(<Component />, container);
});

// ❌ Bad - effects may not be flushed
act(() => {
  render(<Component />, container);
});

Wrap all renders and updates

// ✅ Good
await act(() => {
  render(<Component />, container);
});

await act(() => {
  button.click();
});

// ❌ Bad - update may not flush
await act(() => {
  render(<Component />, container);
});
button.click(); // Not wrapped!

Use with testing libraries

import { render, fireEvent } from '@testing-library/preact';
// @testing-library/preact wraps act automatically

Build docs developers (and LLMs) love