Lifting State Up in React – Parent-Child State Management (2025 Guide)

In React, data flows in one direction — from parent to child through props.

Sometimes, two or more sibling components need to share the same state.

Instead of duplicating state in each component, React encourages lifting state up to the closest common ancestor.

Lifting state up ensures that the shared state lives in one place, making your app predictable and easier to maintain.

Why Lift State Up?

  • Avoid duplicated state in multiple components

  • Synchronize related components that depend on the same data

  • Maintain single source of truth for consistent UI

Common examples:

  • Two input fields showing the same data

  • Tabs that need to communicate selected state

  • Form components that update a shared summary

Example: Two Inputs Sharing the Same State

We’ll create a temperature converter with Celsius and Fahrenheit inputs. Both inputs must stay in sync.

Step 1: Create Child Components

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

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

Each input component receives state and a callback function as props.

Step 2: Lift State Up to Parent Component

import React, { useState } from "react";
import CelsiusInput from "./CelsiusInput";
import FahrenheitInput from "./FahrenheitInput";

function TemperatureConverter() {
  const [celsius, setCelsius] = useState("");
  const [fahrenheit, setFahrenheit] = useState("");

  const handleCelsiusChange = (value) => {
    setCelsius(value);
    setFahrenheit(((parseFloat(value) * 9) / 5 + 32).toFixed(2));
  };

  const handleFahrenheitChange = (value) => {
    setFahrenheit(value);
    setCelsius(((parseFloat(value) - 32) * 5 / 9).toFixed(2));
  };

  return (
    <div>
      <CelsiusInput temperature={celsius} onTemperatureChange={handleCelsiusChange} />
      <FahrenheitInput temperature={fahrenheit} onTemperatureChange={handleFahrenheitChange} />
    </div>
  );
}

export default TemperatureConverter;

How It Works

  1. The state lives in the parent (TemperatureConverter).

  2. Child components receive state and update functions as props.

  3. When a child changes the value:

    • It calls the callback (onTemperatureChange)

    • The parent updates state

    • Both children receive updated props → stay in sync

 This is lifting state up — moving state from children to the parent so it can be shared.

Key Principles

  • Always lift state to the closest common ancestor of all components that need it.

  • Pass state and updater functions as props to children.

  • Avoid keeping the same state in multiple places.

Another Example: Checkbox Group

function Checkbox({ label, checked, onChange }) {
  return (
    <label>
      <input type="checkbox" checked={checked} onChange={onChange} />
      {label}
    </label>
  );
}

function CheckboxGroup() {
  const [checkedItems, setCheckedItems] = useState({ apple: false, banana: false });

  const handleChange = (item) => {
    setCheckedItems({ ...checkedItems, [item]: !checkedItems[item] });
  };

  return (
    <div>
      <Checkbox label="Apple" checked={checkedItems.apple} onChange={() => handleChange("apple")} />
      <Checkbox label="Banana" checked={checkedItems.banana} onChange={() => handleChange("banana")} />
    </div>
  );
}
  • State (checkedItems) lives in parent

  • Children just trigger updates via props

  • This prevents inconsistent states across checkboxes

Best Practices

  • Only lift state when multiple components depend on it

  • For complex or large apps, consider Context API or Redux

  • Keep the parent responsible for the source of truth

  • Use descriptive prop names (value, onChange) for clarity