How to Test Uncontrolled Components in React
Testing uncontrolled components in React requires a different approach than testing controlled ones because uncontrolled components rely on the DOM itself to store and manage their state. This article provides a straightforward guide on how to test uncontrolled React components using React Testing Library and Jest, focusing on simulating real user interactions and asserting that the DOM state is updated correctly.
Understanding Uncontrolled Components
In an uncontrolled component, form data is handled by the DOM rather
than by React state. Instead of writing an event handler for every state
update, you use a ref to pull values from the DOM when you
need them.
Because the component state is managed internally by the DOM, unit tests must interact directly with DOM nodes to set values and trigger events, mimicking how a real user interacts with the application.
Step-by-Step Testing Guide
To test an uncontrolled component, you need to render the component, query the DOM elements, simulate user input, and assert that the correct values are captured upon an action (like a form submission).
Here is a simple uncontrolled form component:
import React, { useRef } from 'react';
function UncontrolledForm({ onSubmit }) {
const inputRef = useRef(null);
const handleSubmit = (event) => {
event.preventDefault();
onSubmit(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="username">Username</label>
<input id="username" type="text" ref={inputRef} />
<button type="submit">Submit</button>
</form>
);
}
export default UncontrolledForm;Writing the Test
Using React Testing Library and
@testing-library/user-event, you can write a test that
simulates a user typing into the input field and submitting the
form.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import UncontrolledForm from './UncontrolledForm';
test('submits the correct value from uncontrolled input', async () => {
const mockSubmit = jest.fn();
render(<UncontrolledForm onSubmit={mockSubmit} />);
// Locate the input and button elements
const inputElement = screen.getByLabelText(/username/i);
const submitButton = screen.getByRole('button', { name: /submit/i });
// Simulate user typing into the uncontrolled input
await userEvent.type(inputElement, 'JSDeveloper');
// Simulate form submission
await userEvent.click(submitButton);
// Assert that the submission handler was called with the correct value
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith('JSDeveloper');
});Key Takeaways for Testing Uncontrolled Components
- Use User-Event: Always prefer
@testing-library/user-eventoverfireEvent.userEventsimulates full browser interactions, which ensures that the underlying DOM APIs used by uncontrolled components are triggered correctly. - Query by Accessibility Labels: Use queries like
getByLabelTextorgetByRoleto find form elements. This keeps your tests accessible and resilient to structural changes in your HTML. - Assert on Callback Functions: Since uncontrolled components don’t update React state on every keystroke, verify their behavior by asserting on the arguments passed to submit or change handlers.