Back to all notes

Lifting State Up: Share Data Between Siblings Without Chaos

Learn how to move state to a common parent so sibling components can stay simple and in sync.

3 min read

Introduction

Lifting state up means moving state to the closest common parent of components that need it. This creates a single source of truth so siblings stay in sync.

Why this matters

Two or more components need to read or update the same piece of data. If each component owns its own copy, they’ll drift out of sync. You’ll also end up passing too many callbacks around.

The fix is to move the state up to the closest common parent and pass it down as props. Each child becomes a pure, predictable component.

The problem

A common mistake is letting each sibling keep its own copy of shared data. For example, an input manages its own name, while the display component has no way to access it. The result is drift and duplicated logic.

Inefficient approach

Each component manages its own state, so siblings can’t share data:

import { useState } from "react";

// NameInput owns its own state — siblings can't share it
function NameInput() {
  const [name, setName] = useState("");
  return (
    <div>
      <label>Enter your name:</label>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </div>
  );
}

// NameDisplay doesn't know the current name
function NameDisplay() {
  return (
    <div>
      <p>Hello, {}!</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Lifting State Up Example</h1>
      <NameInput />
      <NameDisplay />
    </div>
  );
}

export default App;

The solution

Lift the name state to the parent (App) and pass it to both children. NameInput becomes a controlled component, and NameDisplay renders whatever it receives.

import { useState } from "react";

function NameInput({ name, setName }) {
  return (
    <div>
      <label>Enter your name:</label>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </div>
  );
}

function NameDisplay({ name }) {
  return (
    <div>
      <p>Hello, {name}!</p>
    </div>
  );
}

function App() {
  const [name, setName] = useState("");

  return (
    <div>
      <h1>Lifting State Up Example</h1>
      <NameInput name={name} setName={setName} />
      <NameDisplay name={name} />
    </div>
  );
}

Step-by-step

  1. Remove local name state from NameInput.
  2. Add const [name, setName] = useState("") to the parent.
  3. Pass name and setName to NameInput as props.
  4. Pass name to NameDisplay as a prop.
  5. Confirm both children stay in sync through the single source of truth.

Why this is better

  • A single source of truth prevents divergence between siblings.
  • Data flow is explicit and easy to trace from parent to children.
  • Each component has a single responsibility: input updates vs. display.

Edge cases and tips

  • If many deeply nested components need the same value, consider Context to avoid prop drilling.
  • Keep parents lean—if a parent grows large, extract presentational children or use custom hooks for logic.
  • For form inputs, always control both value and onChange to avoid UI drift.

Takeaway

Lift state when multiple components need to stay in sync. Centralizing the source of truth makes your UI predictable and your components easier to test.

Knowledge base

Problem snippet:

function NameInput() {
  const [name, setName] = useState("");
  return <input value={name} onChange={(e) => setName(e.target.value)} />;
}
function NameDisplay() {
  return <p>Hello, {}!</p>;
}

Solution snippet:

function App() {
  const [name, setName] = useState("");
  return (
    <>
      <NameInput name={name} setName={setName} />
      <NameDisplay name={name} />
    </>
  );
}