So far, we’ve only passed data down the component tree using props (unidirectional data flow). However, real applications require interaction: a button clicked in a child component often needs to update the state of a parent or a sibling component.
Lifting State Up is the standard React pattern for enabling communication from a child component back to its parent.
1. The Problem: Sharing State
Imagine you have two sibling components, ComponentA and ComponentB, that both need to react to a button click inside ComponentA. Since sibling components cannot directly communicate, and ComponentA cannot directly change ComponentB‘s props, the state must be moved.
2. The Solution: Lifting State Up
The solution involves moving the shared state to the closest common ancestor (the parent component) that contains both siblings.
- State Ownership: The parent component manages the state using
useState. - Pass Down Data: The parent passes the state value down to the child component(s) that need to read it (e.g., to
ComponentB). - Pass Down Function: The parent passes the state’s updater function (the
set...function) down to the child component(s) that need to change it (e.g., toComponentA). - Child Invocation: The child calls the passed-down updater function as a callback whenever an event occurs.
3. Execution Example: Synchronizing Sibling Inputs
Let’s create a simple app where typing in one input field updates the text in another, controlled by their common parent.
We will use three files:
InputText.js(Child Component: Handles the input event)DisplayBox.js(Sibling Component: Renders the shared state)App.js(Parent Component: Owns the state)
A. InputText.js (The Child Component that needs to communicate)
Create or open InputText.js:
import React from 'react';
// This component receives the parent's setter function as a prop (onInputChange)
function InputText({ onInputChange }) {
const handleChange = (event) => {
// 1. When the input changes, call the function passed down from the parent.
// This lifts the new value up to the parent component.
onInputChange(event.target.value);
};
return (
<div>
<label>
Type Here:
<input type="text" onChange={handleChange} />
</label>
</div>
);
}
export default InputText;
B. DisplayBox.js (The Sibling Component that reads the state)
Create or open DisplayBox.js:
import React from 'react';
// This component receives the shared state value as a prop
function DisplayBox({ sharedText }) {
return (
<div style={{ border: '1px dashed blue', padding: '15px', marginTop: '15px' }}>
<h4>Shared Output:</h4>
<p>{sharedText || 'Start typing...'}</p>
</div>
);
}
export default DisplayBox;
C. App.js (The Closest Common Ancestor)
Open App.js and replace its content with the following:
import React, { useState } from 'react';
import InputText from './InputText';
import DisplayBox from './DisplayBox';
function App() {
// 1. State is declared in the parent (common ancestor)
const [inputText, setInputText] = useState('');
// 2. The callback function that updates the state
const handleTextChange = (newText) => {
setInputText(newText);
};
return (
<div className="StateLiftDemo" style={{ padding: '20px' }}>
<h1>Lifting State Up</h1>
{/* 3. Pass the setter function DOWN to the child that needs to update state */}
<InputText onInputChange={handleTextChange} />
{/* 4. Pass the state VALUE DOWN to the sibling that needs to read it */}
<DisplayBox sharedText={inputText} />
<p style={{ marginTop: '20px', fontSize: 'small' }}>
*State owned by App.js: {inputText}
</p>
</div>
);
}
export default App;
- Local Execution: Run
npm start. - Observation: Typing in the
InputTextcomponent triggers thehandleTextChangecallback inApp.js.App.jsupdates itsinputTextstate, causing bothDisplayBoxandInputTextto re-render, thus synchronizing the sibling components.
