Beginner25 min read

Lifting State Up

Learn how to share state between sibling components by lifting it to their common parent, enabling coordinated behavior across your component tree.

When Siblings Need to Share Data

In React, data flows down from parent to child via props. This is called one-way data flow or unidirectional data flow. But what happens when two sibling components need to share and react to the same data?

Consider a temperature converter where you have two inputs — one for Celsius and one for Fahrenheit. When the user types in the Celsius input, the Fahrenheit display should update, and vice versa. Both components need access to the same temperature value.

You cannot pass data directly between siblings. Instead, you lift the shared state up to their closest common parent component. The parent holds the state and passes it down to both children as props.

html
     Parent (holds state)
      /         \
  ChildA       ChildB
  (reads)      (reads)

This pattern is called lifting state up and it is one of the most fundamental patterns in React. When you find yourself needing to synchronize two components, look for their common parent and move the state there.

Before lifting state (broken):

jsx
// Each component has its own state — they can't communicate
function CelsiusInput() {
  const [celsius, setCelsius] = React.useState(0);
  return <input value={celsius} onChange={...} />;
}

function FahrenheitDisplay() {
  // How does this component know the Celsius value? It can't!
  return <p>???</p>;
}

After lifting state (working):

jsx
function App() {
  const [celsius, setCelsius] = React.useState(0);
  return (
    <>
      <CelsiusInput celsius={celsius} onCelsiusChange={setCelsius} />
      <FahrenheitDisplay celsius={celsius} />
    </>
  );
}

Now the parent owns the data and both children can access it.

Lifting State to the Parent

Here is the step-by-step process for lifting state:

Step 1: Identify the shared state. Determine which state needs to be shared between components. In our temperature example, it is the temperature value.

Step 2: Find the common parent. Find the closest component in the tree that is an ancestor of all components that need the shared state.

Step 3: Move the state to the parent. Remove the state from the child components and add it to the parent using useState.

Step 4: Pass the state down as props. Pass the state value to each child that needs to read it.

Step 5: Pass the update function as a prop. Pass the setter function (or a handler that calls it) to the child that needs to modify the state.

Here is the full example:

jsx
function TemperatureConverter() {
  // Step 3: State lives in the parent
  const [celsius, setCelsius] = React.useState(0);

  return (
    <div>
      {/* Step 4 & 5: Pass state and updater down */}
      <CelsiusInput
        value={celsius}
        onChange={setCelsius}
      />
      <FahrenheitDisplay celsius={celsius} />
    </div>
  );
}

function CelsiusInput({ value, onChange }) {
  return (
    <label>
      Celsius:
      <input
        type="number"
        value={value}
        onChange={(e) => onChange(Number(e.target.value))}
      />
    </label>
  );
}

function FahrenheitDisplay({ celsius }) {
  const fahrenheit = celsius * 9 / 5 + 32;
  return <p>{fahrenheit.toFixed(1)}°F</p>;
}

The CelsiusInput does not own the temperature state — it receives the value and calls onChange when the user types. The parent receives the update, changes its state, and both children re-render with the new value.

Passing Update Functions as Props

When a child component needs to modify the parent's state, the parent passes down a callback function. This is how data flows up in React — through callbacks.

jsx
function Parent() {
  const [items, setItems] = React.useState(['Item 1', 'Item 2']);

  function addItem(text) {
    setItems([...items, text]);
  }

  function removeItem(index) {
    setItems(items.filter((_, i) => i !== index));
  }

  return (
    <div>
      <AddItemForm onAdd={addItem} />
      <ItemList items={items} onRemove={removeItem} />
    </div>
  );
}

function AddItemForm({ onAdd }) {
  const [text, setText] = React.useState('');

  function handleSubmit(e) {
    e.preventDefault();
    if (text.trim()) {
      onAdd(text); // Call the parent's function
      setText('');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button type="submit">Add</button>
    </form>
  );
}

function ItemList({ items, onRemove }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>
          {item}
          <button onClick={() => onRemove(index)}>Remove</button>
        </li>
      ))}
    </ul>
  );
}

Key points:

  • The child calls the callback (onAdd, onRemove) provided by the parent.
  • The parent's function updates the state.
  • React re-renders the parent and both children with the new state.
  • The child does not need to know how the state is stored or updated — it just calls the callback.

This keeps the child components reusable. They work with any parent that provides the expected callback props.

Single Source of Truth

The principle behind lifting state is the single source of truth. For any piece of data that changes in your application, there should be exactly one component that "owns" it in its state. All other components that need this data should receive it via props.

Why is this important?

  1. No data inconsistency. If two components each have their own copy of the same data, they can get out of sync. With a single source, there is only one value and it is always consistent.

  2. Easier debugging. When something goes wrong, you only need to look at one place to find the source of the data.

  3. Predictable updates. All state changes go through one path, making the data flow easy to trace.

Example of duplicated state (bad):

jsx
// Bad: both children have their own copy of "name"
function Parent() {
  return (
    <>
      <NameInput />  {/* Has its own name state */}
      <Greeting />   {/* Has its own name state */}
    </>
  );
}
// If NameInput updates, Greeting doesn't know about it!

Example of single source of truth (good):

jsx
// Good: parent owns the state, children receive via props
function Parent() {
  const [name, setName] = React.useState('');
  return (
    <>
      <NameInput name={name} onNameChange={setName} />
      <Greeting name={name} />
    </>
  );
}
// Both children always show the same name

How to identify where state should live:

  1. Identify every component that renders something based on that state.
  2. Find the closest common parent of all those components.
  3. That common parent (or a component above it) should own the state.

Component Communication Patterns

As your application grows, components need to communicate in various ways. Here is a summary of the main patterns:

Parent to child (props): The most straightforward pattern. The parent passes data down via props.

jsx
<UserCard name={user.name} email={user.email} />

Child to parent (callbacks): The parent passes a function as a prop. The child calls it to send data up.

jsx
// Parent
function Parent() {
  function handleSelect(item) {
    console.log('Selected:', item);
  }
  return <Menu onSelect={handleSelect} />;
}

// Child
function Menu({ onSelect }) {
  return <button onClick={() => onSelect('about')}>About</button>;
}

Sibling to sibling (lifting state): Lift shared state to the common parent and pass it down to both siblings.

jsx
function Parent() {
  const [selected, setSelected] = React.useState(null);
  return (
    <>
      <Sidebar items={items} onSelect={setSelected} />
      <Detail selected={selected} />
    </>
  );
}

When lifting state becomes cumbersome: If you find yourself passing props through many layers of components that do not use the data themselves (called prop drilling), it may be time to consider:

  • React Context — provides a way to share data without passing props through every level.
  • State management libraries — like Zustand or Redux, for complex state that many components need.

These are more advanced topics, but the foundation is always the same: one component owns the state, and other components access it through a well-defined interface.

Lifting State in Action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  const { useState } = React;

  function CelsiusInput({ value, onChange }) {
    return (
      <label>
        Celsius:
        <input
          type="number"
          value={value}
          onChange={(e) => onChange(Number(e.target.value))}
        />
      </label>
    );
  }

  function FahrenheitDisplay({ celsius }) {
    const fahrenheit = (celsius * 9) / 5 + 32;
    return <p>Fahrenheit: {fahrenheit.toFixed(1)}&deg;F</p>;
  }

  function App() {
    const [celsius, setCelsius] = useState(0);

    return (
      <div>
        <h2>Temperature Converter</h2>
        <CelsiusInput value={celsius} onChange={setCelsius} />
        <FahrenheitDisplay celsius={celsius} />
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

When two sibling components need to share state, where should that state live?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.