Skip to main content

Lifting State Up: Share State Between Components

Lifting state up is the canonical pattern for sharing data between sibling components in React. Instead of allowing each component to manage its own copy of the same data, you move (or "lift") the state to their closest common ancestor and pass it down via props. This creates a single source of truth, ensures data consistency, and enables synchronization across multiple components. This technique is fundamental to building scalable React applications.

Key Takeaways

  • Lifting state up is the solution to sharing data between sibling components without prop drilling
  • The single source of truth principle means only one component owns and manages shared state
  • Data flows down from parent to child via props; changes flow up via callback functions
  • Controlled components receive their values from parent state and communicate changes through callbacks
  • Identify the closest common ancestor to minimize unnecessary prop passing through intermediate layers

What Is Lifting State Up and Why Do You Need It?

Lifting state up is the process of moving state to the closest common ancestor of the components that need it. Instead of each sibling having its own local copy of the same data, the parent owns the state and passes it down.

Think of it as a shared toy: instead of each sibling keeping their own version and constantly checking if it matches the other's, the parent holds the toy and shares it with whichever sibling needs it. If one sibling changes it, the parent sees the update and can share the new version with the other sibling.

Key Principles:

  • Single Source of Truth: One component owns the state; others receive it via props.
  • Data Flows Down: The owner passes state to children via props.
  • Events Flow Up: Children call callback functions (passed as props) to notify the parent of changes.

What Is a Controlled Component?

A controlled component has its value driven by React state passed from a parent via props, and it communicates changes back to the parent through callback functions. The parent is always in control.

function TemperatureInput({ scale, temperature, onTemperatureChange }) {
return (
<fieldset>
<legend>Enter temperature in {scale}:</legend>
<input
value={temperature}
onChange={(e) => onTemperatureChange(e.target.value)}
/>
</fieldset>
);
}

Breakdown:

  • value={temperature} — The input's value comes from the parent's props (not local state).
  • onChange={(e) => onTemperatureChange(e.target.value)} — When the user types, the component calls the parent's callback; it does not update local state.

This is the opposite of an uncontrolled component, which manages its own internal state and is harder to synchronize across multiple components.

How Do You Build a Temperature Converter with Lifted State?

A temperature converter with two inputs (Celsius and Fahrenheit) is the ideal example: changing one must instantly update the other. This requires lifting state and synchronization logic to the parent.

Step 1: Helper Functions for Conversion

function toCelsius(fahrenheit) {
return ((fahrenheit - 32) * 5) / 9;
}

function toFahrenheit(celsius) {
return (celsius * 9) / 5 + 32;
}

function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}

Step 2: Controlled Input Component

function TemperatureInput({ scale, temperature, onTemperatureChange }) {
const scaleNames = { c: 'Celsius', f: 'Fahrenheit' };

return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input
value={temperature}
onChange={(e) => onTemperatureChange(e.target.value)}
/>
</fieldset>
);
}

Step 3: Parent Component with Lifted State

import React, { useState } from 'react';

function Calculator() {
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c');

const handleCelsiusChange = (temp) => {
setScale('c');
setTemperature(temp);
};

const handleFahrenheitChange = (temp) => {
setScale('f');
setTemperature(temp);
};

const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;

return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={handleCelsiusChange}
/>
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={handleFahrenheitChange}
/>
</div>
);
}

Walkthrough:

  • State in parent: Calculator owns temperature and scale. scale tracks which input was last edited.
  • Handlers: handleCelsiusChange and handleFahrenheitChange update parent state when children call them.
  • Conversion logic: Before rendering, Calculator calculates the value for both inputs. If last edit was Celsius, it converts to Fahrenheit; if Fahrenheit, it converts to Celsius.
  • Controlled children: Both TemperatureInput components receive state from the parent and their handler functions.

Best Practices for Lifting State

  • Identify the closest common ancestor: Find the parent component closest to all children that need the state. Do not lift higher than necessary.
  • Keep state colocation: State should live as close as possible to where it is used. Only lift it when multiple components genuinely need to share it.
  • Prefer controlled components: When state is shared, use controlled components (no local state, values from props, changes via callbacks).
  • Avoid lifting state too high: Lifting to the top-level component "just in case" creates the same problems as prop drilling, forcing you to pass props through many uninterested layers.

Frequently Asked Questions

What is the difference between a controlled and uncontrolled component?

A controlled component receives its value from parent props and calls a callback to notify the parent of changes. The parent is always in control. An uncontrolled component manages its own state internally. Use controlled components when you need to share state or synchronize multiple inputs.

How do you decide where to lift state?

Find the closest ancestor that is a parent of all components needing the state. If two sibling components share state, lift to their immediate parent. If nested children need it, lift to their common ancestor—but not higher, as this causes prop drilling.

Can you lift state too high in the component tree?

Yes. Lifting state too high (e.g., to the root component) means passing it as props through many intermediate components that do not use it. This creates unnecessary prop drilling and makes the code harder to refactor. Lift only as high as necessary.

What if three or more components need to share the same state?

Lift state to their closest common ancestor and pass it down to all children via props and callbacks. The pattern is the same: one parent owns the state, children are controlled, and updates flow up through handlers.

How do you handle performance when lifting state?

If lifting state causes the parent to re-render many times (e.g., on every keystroke), consider using the useMemo hook to memoize derived values, or useCallback to memoize callback functions so children do not re-render unnecessarily.

Further Reading