How to Test Lifting State Up in React
Testing the “lifting state up” pattern in React requires verifying that a parent component successfully coordinates state between its child components. To do this effectively, you should write integration tests that render the parent component, simulate user interactions in one child component, and assert that the resulting state changes are correctly reflected in another child component. This guide demonstrates how to test this pattern using React Testing Library and Jest.
The Strategy: Component Integration Testing
When state is lifted, the individual child components become “controlled” components that rely on props passed from the parent. Testing these children in isolation using mocked functions is useful, but the most reliable way to test lifted state is to test the parent and children together as a single unit. This ensures that the state flow actually works in runtime.
Example Scenario
Consider a parent component, TemperatureCalculator,
which holds the state. It has two child components: 1.
TemperatureInput: A text input where the user types a
temperature. 2. BoilingVerdict: A display component that
shows whether the water would boil at that temperature.
Step-by-Step Testing Implementation
1. The Components Under Test
Here is the implementation of the components we want to test:
// TemperatureInput.js
export function TemperatureInput({ temperature, onTemperatureChange }) {
return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input
value={temperature}
onChange={(e) => onTemperatureChange(e.target.value)}
placeholder="Enter temp"
/>
</fieldset>
);
}
// BoilingVerdict.js
export function BoilingVerdict({ celsius }) {
if (parseFloat(celsius) >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
// TemperatureCalculator.js (Parent holding the lifted state)
import { useState } from 'react';
import { TemperatureInput } from './TemperatureInput';
import { BoilingVerdict } from './BoilingVerdict';
export default function TemperatureCalculator() {
const [temperature, setTemperature] = useState('');
return (
<div>
<TemperatureInput
temperature={temperature}
onTemperatureChange={setTemperature}
/>
<BoilingVerdict celsius={temperature} />
</div>
);
}2. Writing the Integration Test
To test the lifted state, render the
TemperatureCalculator parent component. Use
user-event to type into the input field (handled by
TemperatureInput), and assert that the text in
BoilingVerdict updates accordingly.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TemperatureCalculator from './TemperatureCalculator';
describe('TemperatureCalculator - Lifted State Integration', () => {
test('updates boiling verdict when temperature input changes', async () => {
// 1. Render the parent component
render(<TemperatureCalculator />);
// 2. Locate the input field from the child component
const inputElement = screen.getByPlaceholderText(/enter temp/i);
// Check initial state
expect(screen.getByText(/the water would not boil/i)).toBeInTheDocument();
// 3. Simulate user typing a boiling temperature
await userEvent.type(inputElement, '105');
// 4. Assert that the sibling child component has updated
expect(screen.getByText(/the water would boil/i)).toBeInTheDocument();
expect(screen.queryByText(/the water would not boil/i)).not.toBeInTheDocument();
});
});3. Testing Child Components in Isolation (Optional)
While integration testing is the priority, you should also write brief unit tests for the child components to ensure they behave correctly when receiving props.
For the input component, verify it calls its callback prop:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TemperatureInput } from './TemperatureInput';
test('calls onTemperatureChange when user types', async () => {
const handleChanceMock = jest.fn();
render(
<TemperatureInput
temperature=""
onTemperatureChange={handleChanceMock}
/>
);
const inputElement = screen.getByPlaceholderText(/enter temp/i);
await userEvent.type(inputElement, '5');
expect(handleChanceMock).toHaveBeenCalledWith('5');
});For the display component, verify it outputs the correct text based on the prop:
import { render, screen } from '@testing-library/react';
import { BoilingVerdict } from './BoilingVerdict';
test('renders correct verdict for boiling temperature', () => {
render(<BoilingVerdict celsius="100" />);
expect(screen.getByText(/the water would boil/i)).toBeInTheDocument();
});