Mocking Uncontrolled Components in React
Mocking uncontrolled components in React is a crucial technique for isolating components during unit testing. Since uncontrolled components rely on the DOM and React refs to manage their own internal state rather than props, testing their integration requires specific mocking strategies. This article explains how to mock these components using Jest and React Testing Library, focusing on replacing the component with a mock implementation and simulating DOM interactions.
Why Mock Uncontrolled Components?
Uncontrolled components, such as a standard HTML
<input type="file" /> or third-party rich text
editors, manage their own state internally. In unit tests, you often
want to test how the parent component reacts to changes in these
children without dealing with their complex internal DOM manipulations.
Mocking these components simplifies your test suite, rendering a
predictable mock instead of the actual uncontrolled element.
Method 1: Mocking the Component with Jest
The most robust way to handle an uncontrolled child component is to
mock the entire module using jest.mock(). By replacing the
uncontrolled component with a simple controlled mock, you can easily
trigger events and verify if the parent component behaves correctly.
Assume you have an uncontrolled component named
UncontrolledForm:
// UncontrolledForm.jsx
import React, { useRef } from 'react';
export default function UncontrolledForm({ onSubmit }) {
const inputRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} />
<button type="submit">Submit</button>
</form>
);
}You can mock this component in your test file so it behaves like a controlled component, making it easy to pass values:
// ParentComponent.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import ParentComponent from './ParentComponent';
// Mock the uncontrolled component
jest.mock('./UncontrolledForm', () => {
return function MockUncontrolledForm({ onSubmit }) {
return (
<div data-testid="mock-form">
<input
data-testid="mock-input"
onChange={(e) => onSubmit(e.target.value)}
/>
</div>
);
};
});
test('should receive data from mocked uncontrolled component', () => {
const handleData = jest.fn();
render(<ParentComponent onDataReceived={handleData} />);
const mockInput = screen.getByTestId('mock-input');
fireEvent.change(mockInput, { target: { value: 'Test Value' } });
expect(handleData).toHaveBeenCalledWith('Test Value');
});Method 2: Mocking React.useRef
If you cannot mock the entire component and must test the file
containing the ref directly, you can spy on and mock
React.useRef. This allows you to control the
.current property that the uncontrolled component relies
on.
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import UncontrolledForm from './UncontrolledForm';
test('should submit the mocked ref value', () => {
const mockSubmit = jest.fn();
// Spy on useRef and return a custom current object
const refSpy = jest.spyOn(React, 'useRef').mockReturnValue({
current: { value: 'Mocked Input Value' }
});
const { getByRole } = render(<UncontrolledForm onSubmit={mockSubmit} />);
const submitButton = getByRole('button', { name: /submit/i });
fireEvent.click(submitButton);
expect(mockSubmit).toHaveBeenCalledWith('Mocked Input Value');
refSpy.mockRestore();
});Using these two approaches, you can easily bypass the limitations of testing uncontrolled elements, ensuring your React tests remain fast, reliable, and isolated.