CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-testing-library--react-hooks

Simple and complete React hooks testing utilities that encourage good testing practices.

Pending
Overview
Eval results
Files

cleanup-management.mddocs/

Cleanup Management

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.

Capabilities

cleanup Function

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();
});

addCleanup Function

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
});

removeCleanup Function

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
});

Automatic Cleanup

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
});

Disabling Auto-Cleanup

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 Error Handling

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();
});

Best Practices

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

docs

act-utilities.md

async-testing.md

cleanup-management.md

error-handling.md

hook-rendering.md

index.md

server-side-rendering.md

tile.json