Simple and complete React hooks testing utilities that encourage good testing practices.
—
State update wrapping utilities that ensure proper timing and batching of React updates during testing. The act utility is essential for testing hooks that perform state updates or side effects.
Wraps code that causes React state updates, ensuring that all updates are flushed and effects are executed before the act call completes. This is crucial for predictable testing of hooks.
/**
* Wraps synchronous code that causes state updates
* @param callback - Synchronous function that triggers state updates
*/
function act(callback: () => void | undefined): void;
/**
* Wraps asynchronous code that causes state updates
* @param callback - Asynchronous function that triggers state updates
* @returns Promise that resolves when all updates are complete
*/
function act(callback: () => Promise<void | undefined>): Promise<undefined>;Usage Examples:
import { renderHook, act } from "@testing-library/react-hooks";
import { useState } from "react";
function useCounter(initialCount = 0) {
const [count, setCount] = useState(initialCount);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
return { count, increment, decrement };
}
test("synchronous state updates with act", () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
// Wrap synchronous state updates in act
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(3);
});
// Asynchronous example
function useAsyncCounter() {
const [count, setCount] = useState(0);
const incrementAsync = async () => {
await new Promise(resolve => setTimeout(resolve, 100));
setCount(prev => prev + 1);
};
return { count, incrementAsync };
}
test("asynchronous state updates with act", async () => {
const { result } = renderHook(() => useAsyncCounter());
expect(result.current.count).toBe(0);
// Wrap asynchronous state updates in act
await act(async () => {
await result.current.incrementAsync();
});
expect(result.current.count).toBe(1);
});Use act whenever you're triggering state updates or side effects in your hooks during tests:
State Updates:
// ✅ Correct - wrap state updates
act(() => {
result.current.setValue("new value");
});
// ❌ Incorrect - unwrapped state update
result.current.setValue("new value");Effect Triggers:
function useDebounce(value: string, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
test("debounce hook", async () => {
jest.useFakeTimers();
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: "initial", delay: 500 } }
);
expect(result.current).toBe("initial");
// Update props that trigger useEffect
act(() => {
rerender({ value: "updated", delay: 500 });
});
// Fast-forward timers
act(() => {
jest.advanceTimersByTime(500);
});
expect(result.current).toBe("updated");
});Event Handlers:
function useClickCounter() {
const [count, setCount] = useState(0);
const [ref, setRef] = useState<HTMLElement | null>(null);
useEffect(() => {
if (!ref) return;
const handleClick = () => setCount(prev => prev + 1);
ref.addEventListener("click", handleClick);
return () => ref.removeEventListener("click", handleClick);
}, [ref]);
return { count, ref: setRef };
}
test("click counter hook", () => {
const { result } = renderHook(() => useClickCounter());
const element = document.createElement("button");
act(() => {
result.current.ref(element);
});
expect(result.current.count).toBe(0);
act(() => {
element.click();
});
expect(result.current.count).toBe(1);
});act can wrap multiple state updates that should be batched together:
function useMultiState() {
const [name, setName] = useState("");
const [age, setAge] = useState(0);
const [email, setEmail] = useState("");
const updateAll = (newName: string, newAge: number, newEmail: string) => {
setName(newName);
setAge(newAge);
setEmail(newEmail);
};
return { name, age, email, updateAll };
}
test("multiple state updates", () => {
const { result } = renderHook(() => useMultiState());
act(() => {
result.current.updateAll("Alice", 25, "alice@example.com");
});
expect(result.current.name).toBe("Alice");
expect(result.current.age).toBe(25);
expect(result.current.email).toBe("alice@example.com");
});If an error occurs within act, it will be propagated:
function useErrorHook() {
const [shouldError, setShouldError] = useState(false);
const triggerError = () => setShouldError(true);
if (shouldError) {
throw new Error("Hook error occurred");
}
return { triggerError };
}
test("error handling in act", () => {
const { result } = renderHook(() => useErrorHook());
expect(() => {
act(() => {
result.current.triggerError();
});
}).toThrow("Hook error occurred");
});Install with Tessl CLI
npx tessl i tessl/npm-testing-library--react-hooks