Mocking useImperativeHandle Hook in React

Testing components that use React’s useImperativeHandle hook can be challenging because they expose internal functions to parent components via refs. This article explains how to mock useImperativeHandle during testing, detailing how to isolate component behavior using Jest and React Testing Library. You will learn how to mock the ref methods when testing a parent component and how to test the hook’s implementation directly.

Understanding the Challenge

The useImperativeHandle hook customizes the instance value that is exposed to parent components when using ref. Because this couples the parent and child components through a custom API surface, testing requires you to either mock the child’s exposed ref methods or mock the child component entirely during parent-level unit tests.

Approach 1: Mocking the Child Component for Parent Tests

When testing a parent component, you do not need to test the actual implementation of the child component. Instead, you can mock the child component and its useImperativeHandle output using Jest.

Consider a child component named CustomInput that exposes a focusInput method:

// CustomInput.js
import React, { forwardRef, useImperativeHandle, useRef } from 'react';

const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focusInput: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} {...props} />;
});

export default CustomInput;

To test a parent component that calls focusInput, mock CustomInput at the top of your test file:

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ParentComponent from './ParentComponent';

// Mock the child component and its imperative handle
const mockFocusInput = jest.fn();

jest.mock('./CustomInput', () => {
  const React = require('react');
  return React.forwardRef((props, ref) => {
    React.useImperativeHandle(ref, () => ({
      focusInput: mockFocusInput,
    }));
    return <input data-testid="mock-input" />;
  });
});

test('parent component triggers focusInput on child', async () => {
  render(<ParentComponent />);
  
  const button = screen.getByRole('button', { name: /focus input/i });
  await userEvent.click(button);

  // Verify that the mocked hook method was called
  expect(mockFocusInput).toHaveBeenCalledTimes(1);
});

Approach 2: Testing the Hook Directly in the Child

If you want to verify that the child component correctly exposes its functions via useImperativeHandle without mocking the hook itself, you can pass a React ref in your test and assert against it.

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

test('exposes the focusInput method to the parent ref', () => {
  const ref = createRef();
  render(<CustomInput ref={ref} />);

  // Assert that the handle contains the custom method
  expect(ref.current).toBeDefined();
  expect(typeof ref.current.focusInput).toBe('function');
});

Using these two approaches, you can isolate your components during unit tests, ensuring that your imperative APIs are both correctly exposed by child components and correctly invoked by parent components.