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

act-utilities.mddocs/

Act Utilities

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.

Capabilities

act Function

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

When to Use act

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 with Multiple Updates

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

Error Handling in act

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

docs

act-utilities.md

async-testing.md

cleanup-management.md

error-handling.md

hook-rendering.md

index.md

server-side-rendering.md

tile.json