How to Mock Lifting State Up in React
Mocking “lifting state up” in React involves testing a child component in isolation by passing mock functions as props to simulate the parent component’s state handlers. This article explains how to isolate child components, set up mock callback functions using Jest and React Testing Library, and assert that these callbacks are triggered correctly when user events occur.
Understanding the Pattern
In React, “lifting state up” occurs when two or more child components need access to the same changing data. The state is moved to their closest common ancestor (the parent). The parent then passes the state down as read-only props, along with callback functions to update that state.
When testing a child component in isolation, you do not need to render the parent component or manage real React state. Instead, you mock the parent’s state-updating callback function.
Step-by-Step Mocking Guide
To mock the lifted state, we replace the parent’s state-setting
function with a spy or mock function (such as jest.fn() in
Jest or vi.fn() in Vitest).
1. The Child Component Under Test
Consider a SearchInput component that lifts its state up
to a parent component whenever a user types a query.
// SearchInput.jsx
import React from 'react';
export default function SearchInput({ query, onQueryChange }) {
return (
<div>
<label htmlFor="search">Search:</label>
<input
id="search"
type="text"
value={query}
onChange={(e) => onQueryChange(e.target.value)}
/>
</div>
);
}2. Writing the Mock Test
In the test suite, you define a mock function to act as
onQueryChange. You then render the child component,
simulate a user interaction, and assert that the mock function was
called with the expected arguments.
// SearchInput.test.jsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SearchInput from './SearchInput';
describe('SearchInput Component', () => {
it('calls onQueryChange with the correct value when the user types', async () => {
// Create a mock function for the lifted state handler
const mockOnQueryChange = jest.fn();
const testQuery = 'React';
// Render the child component in isolation
render(
<SearchInput
query=""
onQueryChange={mockOnQueryChange}
/>
);
// Find the input element
const inputElement = screen.getByLabelText(/search/i);
// Simulate user typing
await userEvent.type(inputElement, testQuery);
// Assert that the mock function was called
expect(mockOnQueryChange).toHaveBeenCalledTimes(testQuery.length);
expect(mockOnQueryChange).toHaveBeenLastCalledWith(testQuery);
});
});Key Assertions to Use
When verifying that the lifted state logic works correctly, use the following assertions on your mock function:
toHaveBeenCalled(): Verifies that the child component successfully triggered the event handler.toHaveBeenCalledWith(value): Verifies that the child component passed the correct updated state value back up to the parent.toHaveBeenCalledTimes(number): Ensures the handler is not being fired excessively, which helps prevent unwanted re-renders.