How to Test React Refs

Testing React refs is essential for ensuring your application correctly interacts with the DOM to manage focus, trigger animations, or integrate with third-party libraries. This article provides a direct, practical guide on how to test React refs using Jest and React Testing Library. You will learn the recommended patterns for testing DOM-bound refs, testing forwarded refs, and asserting on ref-based behaviors without exposing internal implementation details.

Test Ref Behavior (Focus Management)

The recommended way to test refs when using React Testing Library is to test the observable behavior rather than the ref implementation itself. If a ref is used to focus an input element, your test should assert that the input element receives focus.

The Component

import React, { useRef } from 'react';

export function FocusInput() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} data-testid="my-input" />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

The Test

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

test('focuses the input element when the button is clicked', async () => {
  render(<FocusInput />);

  const input = screen.getByTestId('my-input');
  const button = screen.getByRole('button', { name: /focus input/i });

  // Ensure the input is not focused initially
  expect(input).not.toHaveFocus();

  // Click the button to trigger the ref action
  await userEvent.click(button);

  // Assert that the input now has focus
  expect(input).toHaveFocus();
});

Testing Forwarded Refs

When building reusable UI components, you often need to forward refs to underlying DOM elements using React.forwardRef. To test this, you can pass a created ref from the test environment into your component and assert that the ref is populated with the correct DOM node.

The Component

import React, { forwardRef } from 'react';

export const CustomInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

CustomInput.displayName = 'CustomInput';

The Test

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

test('correctly forwards the ref to the input element', () => {
  const ref = createRef();
  render(<CustomInput ref={ref} placeholder="Enter text" />);

  // Assert that the ref current value is a DOM input element
  expect(ref.current).not.toBeNull();
  expect(ref.current.tagName).toBe('INPUT');
  expect(ref.current.placeholder).toBe('Enter text');
});

Mocking useRef for Implementation Testing

In rare scenarios where you cannot rely on DOM side-effects (such as integrating with external imperative DOM libraries), you can mock useRef to spy on its current property.

Note: Use this approach sparingly, as it tests implementation details rather than user behavior.

The Component

import React, { useRef, useEffect } from 'react';

export function ChartComponent({ data }) {
  const chartRef = useRef(null);

  useEffect(() => {
    if (chartRef.current) {
      // Imperative library call
      chartRef.current.initializeChart(data);
    }
  }, [data]);

  return <div ref={chartRef} data-testid="chart-container" />;
}

The Test

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

test('calls the imperative chart initialization on the ref', () => {
  const mockInitializeChart = jest.fn();
  
  // Create a mock ref object with the spy function attached
  const mockRef = {
    _current: null,
    get current() {
      return this._current;
    },
    set current(val) {
      this._current = val;
      if (val) {
        val.initializeChart = mockInitializeChart;
      }
    }
  };

  // Spy on React.useRef and return our mock ref
  jest.spyOn(React, 'useRef').mockReturnValue(mockRef);

  render(<ChartComponent data={[10, 20, 30]} />);

  // Assert that the imperative method was called on the ref
  expect(mockInitializeChart).toHaveBeenCalledWith([10, 20, 30]);
});