Simple and complete React hooks testing utilities that encourage good testing practices.
npx @tessl/cli install tessl/npm-react-hooks-testing-library@0.6.0React Hooks Testing Library provides simple and complete React hooks testing utilities that encourage good testing practices. It allows you to create a simple test harness for React hooks that handles running them within the body of a function component, providing various utility functions for updating inputs and retrieving outputs of custom hooks.
npm install --save-dev react-hooks-testing-libraryDependencies:
react (peer dependency, ^16.8.0)react-test-renderer (peer dependency, ^16.8.0)import { renderHook, act } from "react-hooks-testing-library";
// testHook is also available but deprecated - use renderHook insteadFor CommonJS:
const { renderHook, act } = require("react-hooks-testing-library");
// testHook is also available but deprecated - use renderHook insteadimport { renderHook, act } from "react-hooks-testing-library";
import { useState, useCallback } from "react";
// Example custom hook
function useCounter() {
const [count, setCount] = useState(0);
const increment = useCallback(() => setCount((x) => x + 1), []);
const decrement = useCallback(() => setCount((x) => x - 1), []);
return { count, increment, decrement };
}
// Test the hook
test("should increment counter", () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
// Test with async operations
test("should handle async updates", async () => {
const { result, waitForNextUpdate } = renderHook(() => useAsyncHook());
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
});Renders a test component that calls the provided hook callback every time it renders.
/**
* Renders a test component that calls the provided callback, including any hooks it calls
* @param callback - Function to call each render that should call one or more hooks for testing
* @param options - Optional configuration object
* @returns Object with result, utilities for testing async hooks, rerendering, and unmounting
*/
function renderHook<P, R>(
callback: (props: P) => R,
options?: {
initialProps?: P;
wrapper?: React.ComponentType;
}
): {
readonly result: {
readonly current: R;
readonly error: Error;
};
readonly waitForNextUpdate: () => Promise<void>;
readonly unmount: () => boolean;
readonly rerender: (hookProps?: P) => void;
};Usage Examples:
// Basic hook testing
const { result } = renderHook(() => useState(0));
expect(result.current[0]).toBe(0);
// Testing hooks with props
const { result, rerender } = renderHook(
({ initialCount }) => useState(initialCount),
{ initialProps: { initialCount: 5 } }
);
expect(result.current[0]).toBe(5);
// Rerender with new props
rerender({ initialCount: 10 });
expect(result.current[0]).toBe(5); // State is preserved, initialCount only used on first render
// Testing hooks that require context
const wrapper = ({ children }) => (
<ThemeProvider theme="dark">{children}</ThemeProvider>
);
const { result } = renderHook(() => useTheme(), { wrapper });
expect(result.current.theme).toBe("dark");Wrapper around react-test-renderer's act function for handling state updates and effects.
/**
* Ensures that updates related to state changes, effects, and event handlers are properly flushed
* @param callback - Function to execute within act scope
* @returns void
*/
function act(callback: () => void): void;Usage Examples:
// Wrapping state updates
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
// Multiple state updates
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(3);Handle asynchronous operations in hooks with proper waiting mechanisms.
/**
* Returns a Promise that resolves the next time the hook renders
* Commonly used when state is updated as the result of an asynchronous action
* @returns Promise that resolves on next hook render
*/
waitForNextUpdate(): Promise<void>;Usage Examples:
// Testing async hooks
const useAsyncData = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData().then((result) => {
setData(result);
setLoading(false);
});
}, []);
return { data, loading };
};
test("should load data asynchronously", async () => {
const { result, waitForNextUpdate } = renderHook(() => useAsyncData());
expect(result.current.loading).toBe(true);
expect(result.current.data).toBe(null);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
expect(result.current.data).toBeDefined();
});Re-render the hook with new props to test how it responds to prop changes.
/**
* Function to rerender the test component including any hooks called in the callback function
* @param newProps - New props to pass to the callback function for future renders
* @returns void
*/
rerender(newProps?: P): void;Usage Examples:
// Testing prop changes
const useGreeting = (name) => {
return `Hello, ${name}!`;
};
const { result, rerender } = renderHook(
({ name }) => useGreeting(name),
{ initialProps: { name: "Alice" } }
);
expect(result.current).toBe("Hello, Alice!");
rerender({ name: "Bob" });
expect(result.current).toBe("Hello, Bob!");Unmount the test component to trigger cleanup effects for useEffect hooks.
/**
* Function to unmount the test component
* Commonly used to trigger cleanup effects for useEffect hooks
* @returns boolean indicating if unmount was successful
*/
unmount(): boolean;Usage Examples:
// Testing cleanup effects
const useTimer = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return count;
};
test("should cleanup timer on unmount", () => {
const { unmount } = renderHook(() => useTimer());
// Verify cleanup doesn't throw errors
expect(() => unmount()).not.toThrow();
});Access errors thrown during hook execution for testing error scenarios.
/**
* Access to any error thrown during hook execution
*/
interface HookResult<R> {
readonly result: {
readonly current: R;
readonly error: Error;
};
}Usage Examples:
// Testing hook errors
const useErrorProneHook = (shouldThrow) => {
if (shouldThrow) {
throw new Error("Something went wrong");
}
return "success";
};
test("should capture hook errors", () => {
const { result } = renderHook(
({ shouldThrow }) => useErrorProneHook(shouldThrow),
{ initialProps: { shouldThrow: true } }
);
expect(result.error).toBeInstanceOf(Error);
expect(result.error.message).toBe("Something went wrong");
});/**
* Options for configuring hook rendering
*/
interface RenderHookOptions<P> {
/** Initial props passed to the hook callback */
initialProps?: P;
/** React component to wrap around the rendered hook */
wrapper?: React.ComponentType;
}
/**
* Result object returned by renderHook
*/
interface RenderHookResult<P, R> {
/** Container for hook return value and any errors */
readonly result: {
/** Current return value of the hook callback */
readonly current: R;
/** Error thrown during hook execution, if any */
readonly error: Error;
};
/** Promise that resolves on next hook render */
readonly waitForNextUpdate: () => Promise<void>;
/** Function to unmount the test component */
readonly unmount: () => boolean;
/** Function to rerender with optional new props */
readonly rerender: (hookProps?: P) => void;
}/**
* @deprecated Use renderHook instead. Will be removed in a future version.
* Legacy alias for renderHook function
*/
const testHook: typeof renderHook;The testHook function is deprecated and shows a console warning. Use renderHook instead for all new code.