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: