How to Mock useCallback Hook in React

Mocking the useCallback hook in React is a highly effective way to control function memoization behavior, isolate components during unit tests, and assert whether specific callbacks are triggered. While standard testing practices usually favor testing component behavior rather than React’s internal hooks, certain scenarios require spying on or overriding useCallback to verify performance optimizations or integration logic. This guide demonstrates how to mock useCallback using Jest and React Testing Library with direct, practical examples.

Why Mock useCallback?

The useCallback hook returns a memoized version of a callback function that only changes if one of the dependencies has changed. You might want to mock it to: * Verify if the memoized function is being called with the correct arguments. * Force the callback to bypass memoization and run on every render during a test. * Simplify the behavior of complex event handlers in child components.


Method 1: Mocking useCallback Globally

If you want to mock useCallback across all tests in a file, you can mock the entire react package. This approach intercepts the hook and returns a simple mock function, while preserving the rest of React’s built-in functionality.

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';

// Mock the react module
jest.mock('react', () => {
  const originalReact = jest.requireActual('react');
  return {
    ...originalReact,
    // Mock useCallback to immediately return the passed function
    useCallback: jest.fn((fn) => fn),
  };
});

describe('Mocking useCallback Globally', () => {
  it('should call the mocked useCallback implementation', () => {
    const { getByRole } = render(<MyComponent />);
    
    // Assertions go here
    expect(React.useCallback).toHaveBeenCalled();
  });
});

In this setup, jest.requireActual('react') ensures that other essential hooks like useState and useEffect continue to work normally, while only useCallback is replaced with a spy.


Method 2: Spying on React.useCallback

Instead of mocking the entire module, you can spy on React.useCallback if you import React as an object in your test file. This is useful when you only want to mock the hook for a specific test case and restore its original behavior afterward.

import React from 'react';
import { render } from '@testing-library/react';
import MyComponent from './MyComponent';

describe('Spying on React.useCallback', () => {
  let useCallbackSpy;

  beforeEach(() => {
    // Create a spy on the useCallback method
    useCallbackSpy = jest.spyOn(React, 'useCallback');
  });

  afterEach(() => {
    // Restore the original implementation after each test
    useCallbackSpy.mockRestore();
  });

  it('should track calls to useCallback', () => {
    render(<MyComponent />);
    
    // Verify that the hook was registered during rendering
    expect(useCallbackSpy).toHaveBeenCalled();
  });

  it('should provide a custom implementation for a single test', () => {
    // Override the hook behavior for this specific test
    useCallbackSpy.mockImplementation((callback) => {
      return (...args) => {
        console.log('Mocked callback executed');
        return callback(...args);
      };
    });

    render(<MyComponent />);
  });
});

Instead of mocking the useCallback hook itself, it is often easier and cleaner to mock the actual function or module that the callback calls. This avoids altering React’s internal rendering engine.

Consider a component that wraps a function inside a useCallback:

// MyComponent.js
import React, { useCallback } from 'react';
import { submitData } from './api';

export default function MyComponent() {
  const handleClick = useCallback(() => {
    submitData();
  }, []);

  return <button onClick={handleClick}>Submit</button>;
}

To test this without touching useCallback, mock the dependency API module:

// MyComponent.test.js
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { submitData } from './api';

// Mock the API module
jest.mock('./api', () => ({
  submitData: jest.fn(),
}));

describe('Testing callback behavior via dependency mocking', () => {
  it('should call submitData when the button is clicked', () => {
    const { getByRole } = render(<MyComponent />);
    
    fireEvent.click(getByRole('button', { name: /submit/i }));
    
    expect(submitData).toHaveBeenCalledTimes(1);
  });
});

This approach tests the actual user-facing behavior and ensures your test suite remains resilient to React framework updates.