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:
- Calls
setupRerender() to enable synchronous rendering
- Overrides
options.requestAnimationFrame to capture effect callbacks
- Executes your callback
- Flushes all pending renders
- Flushes all pending effects
- Repeats until no more work is pending
- 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