Simple and complete React hooks testing utilities that encourage good testing practices.
—
Cleanup utilities for managing test teardown, both automatic and manual cleanup of rendered hooks and associated resources. Proper cleanup prevents memory leaks and ensures test isolation.
Runs all registered cleanup callbacks and clears the cleanup registry. This is typically called automatically after each test, but can be called manually when needed.
/**
* Run all registered cleanup callbacks and clear the registry
* @returns Promise that resolves when all cleanup is complete
*/
function cleanup(): Promise<void>;Usage Examples:
import { renderHook, cleanup } from "@testing-library/react-hooks";
function useTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return count;
}
test("manual cleanup", async () => {
const { result } = renderHook(() => useTimer());
expect(result.current).toBe(0);
// Manually trigger cleanup
await cleanup();
// Hook should be unmounted and timers cleared
});
// Multiple hooks in same test
test("cleanup multiple hooks", async () => {
const { result: result1 } = renderHook(() => useTimer());
const { result: result2 } = renderHook(() => useTimer());
expect(result1.current).toBe(0);
expect(result2.current).toBe(0);
// Cleanup all rendered hooks
await cleanup();
});Registers a custom cleanup callback that will be called during cleanup. Returns a function to remove the callback.
/**
* Add a custom cleanup callback
* @param callback - Function to call during cleanup (can be async)
* @returns Function to remove this cleanup callback
*/
function addCleanup(callback: CleanupCallback): () => void;
type CleanupCallback = () => Promise<void> | void;Usage Examples:
import { renderHook, addCleanup } from "@testing-library/react-hooks";
// Custom resource cleanup
test("custom cleanup callback", async () => {
const mockResource = {
data: "important data",
dispose: jest.fn()
};
// Register custom cleanup
const removeCleanup = addCleanup(() => {
mockResource.dispose();
mockResource.data = null;
});
const { result } = renderHook(() => {
// Hook that uses the resource
return mockResource.data;
});
expect(result.current).toBe("important data");
// Cleanup will be called automatically after test
// or manually with cleanup()
});
// Async cleanup
test("async cleanup callback", async () => {
const mockDatabase = {
connected: true,
disconnect: jest.fn().mockResolvedValue(undefined)
};
addCleanup(async () => {
await mockDatabase.disconnect();
mockDatabase.connected = false;
});
const { result } = renderHook(() => mockDatabase.connected);
expect(result.current).toBe(true);
});
// Conditional cleanup removal
test("remove cleanup callback", () => {
const cleanupFn = jest.fn();
const removeCleanup = addCleanup(cleanupFn);
// Later, if cleanup is no longer needed
removeCleanup();
// cleanupFn will not be called during cleanup
});Removes a previously registered cleanup callback from the cleanup registry.
/**
* Remove a previously registered cleanup callback
* @param callback - The cleanup callback to remove
*/
function removeCleanup(callback: CleanupCallback): void;Usage Examples:
import { renderHook, addCleanup, removeCleanup } from "@testing-library/react-hooks";
test("manual cleanup removal", () => {
const cleanupCallback = jest.fn();
// Add cleanup
addCleanup(cleanupCallback);
// Later, remove it
removeCleanup(cleanupCallback);
// Callback will not be called during cleanup
});
// Cleanup lifecycle management
function useResourceWithCleanup(shouldCleanup: boolean) {
const [resource] = useState(() => createResource());
useEffect(() => {
if (shouldCleanup) {
const cleanup = () => resource.dispose();
addCleanup(cleanup);
return () => removeCleanup(cleanup);
}
}, [shouldCleanup, resource]);
return resource;
}
test("conditional cleanup registration", () => {
const { rerender } = renderHook(
({ shouldCleanup }) => useResourceWithCleanup(shouldCleanup),
{ initialProps: { shouldCleanup: false } }
);
// Initially no cleanup registered
// Enable cleanup
rerender({ shouldCleanup: true });
// Cleanup is now registered
// Disable cleanup
rerender({ shouldCleanup: false });
// Cleanup is removed
});The library automatically registers cleanup for all rendered hooks. This happens through the auto-cleanup system:
// Automatic cleanup is enabled by default
import { renderHook } from "@testing-library/react-hooks";
// Each renderHook call automatically registers cleanup
test("automatic cleanup", () => {
const { result, unmount } = renderHook(() => useState(0));
// Hook will be automatically cleaned up after test
// No manual cleanup needed
});
// Hooks are also cleaned up when explicitly unmounted
test("explicit unmount", () => {
const { result, unmount } = renderHook(() => useState(0));
expect(result.current[0]).toBe(0);
// Explicitly unmount (also removes from cleanup registry)
unmount();
// Hook is now unmounted and cleaned up
});You can disable automatic cleanup by importing a special configuration file:
// At the top of your test file or in setup
import "@testing-library/react-hooks/dont-cleanup-after-each";
// Or require in CommonJS
require("@testing-library/react-hooks/dont-cleanup-after-each");Usage with Manual Cleanup:
// After importing dont-cleanup-after-each
import { renderHook, cleanup } from "@testing-library/react-hooks";
describe("manual cleanup tests", () => {
afterEach(async () => {
// Manually call cleanup after each test
await cleanup();
});
test("hook test 1", () => {
const { result } = renderHook(() => useState(0));
// Test logic...
});
test("hook test 2", () => {
const { result } = renderHook(() => useState(1));
// Test logic...
});
});Cleanup callbacks can handle errors gracefully:
test("cleanup error handling", async () => {
const failingCleanup = jest.fn(() => {
throw new Error("Cleanup failed");
});
const successfulCleanup = jest.fn();
addCleanup(failingCleanup);
addCleanup(successfulCleanup);
// Cleanup continues even if some callbacks fail
await cleanup();
expect(failingCleanup).toHaveBeenCalled();
expect(successfulCleanup).toHaveBeenCalled();
});
// Async cleanup errors
test("async cleanup error handling", async () => {
const failingAsyncCleanup = jest.fn().mockRejectedValue(
new Error("Async cleanup failed")
);
const successfulCleanup = jest.fn();
addCleanup(failingAsyncCleanup);
addCleanup(successfulCleanup);
// All cleanup callbacks run despite errors
await cleanup();
expect(failingAsyncCleanup).toHaveBeenCalled();
expect(successfulCleanup).toHaveBeenCalled();
});Resource Management:
function useFileResource(filename: string) {
const [file, setFile] = useState(null);
useEffect(() => {
const fileHandle = openFile(filename);
setFile(fileHandle);
// Register cleanup for the file handle
const cleanup = () => fileHandle.close();
addCleanup(cleanup);
return () => {
fileHandle.close();
removeCleanup(cleanup);
};
}, [filename]);
return file;
}Test Isolation:
describe("test suite with shared resources", () => {
let sharedResource;
beforeEach(() => {
sharedResource = createSharedResource();
// Register cleanup for shared resource
addCleanup(() => {
sharedResource.dispose();
sharedResource = null;
});
});
test("test 1", () => {
const { result } = renderHook(() => useSharedResource(sharedResource));
// Test logic...
});
test("test 2", () => {
const { result } = renderHook(() => useSharedResource(sharedResource));
// Test logic...
});
});Install with Tessl CLI
npx tessl i tessl/npm-testing-library--react-hooks