React Lifting State Up

As React applications become larger, you will often have multiple components that need to work with the same piece of information.

For example, imagine a page with two components:

        App
       /   \
      ↓     ↓
 Search   Results

The Search component allows the user to enter a search term, while the Results component needs that search term to display matching results.

If both components maintain separate copies of the search term, they can easily become out of sync.

React provides a simple solution: lift the shared state up to their closest common parent.

This technique is called lifting state up.

The basic idea is:

        Parent
       /      \
      ↓        ↓
   Child A   Child B
      ↑        ↓
   updates   receives
      │        │
      └─ shared state ─┘

The parent becomes the owner of the shared state. It can pass the current state to one or more children through props and pass callback functions when children need to request updates.

What Does Lifting State Up Mean?

Lifting state up means moving state from a child component to a common parent component so that multiple components can access and coordinate that state.

Consider two components:

App
├── ComponentA
└── ComponentB

Suppose both components need access to the same value.

Instead of:

ComponentA
  └── own state

ComponentB
  └── separate state

the state can be moved into App:

             App
              │
         Shared State
          /       \
         ↓         ↓
 ComponentA    ComponentB

Now both components work with the same source of truth.

Why Is Lifting State Up Important?

Without lifting state, components that need the same information may maintain separate copies.

For example, imagine a temperature converter with two inputs:

        TemperatureApp
          /        \
         ↓          ↓
    CelsiusInput  FahrenheitInput

If CelsiusInput owns the temperature and FahrenheitInput has its own separate state, changing one input will not automatically update the other.

Instead, the parent can own the temperature:

       TemperatureApp
              │
        temperature
          /       \
         ↓         ↓
    Celsius     Fahrenheit

When one child changes the temperature, the parent updates the shared state and passes the new value to both children.

This keeps the two inputs synchronized.

When Should You Lift State Up?

You should consider lifting state when:

  • Two or more components need the same state.
  • One component needs to affect another component.
  • Multiple components need to stay synchronized.
  • A parent needs to coordinate data between children.
  • You want one source of truth for related data.

You do not need to lift every piece of state.

If only one component uses a piece of state, keeping it inside that component is usually simpler.

The goal is to place state where it can be shared without unnecessarily making unrelated components depend on it.

The Basic Pattern

Lifting state up usually follows a simple pattern.

Move State to the Common Parent

Instead of storing the state in one child:

function ChildA() {
  const [value, setValue] = useState("");
}

move it into the parent:

function App() {
  const [value, setValue] = useState("");
}

Pass the Value Down

The parent passes the current value to the children:

<ChildA value={value} />
<ChildB value={value} />

Pass a Function for Updates

If a child needs to change the value, pass a callback:

<ChildA
  value={value}
  onChange={setValue}
/>

The child can then request a change:

function ChildA({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={(event) => onChange(event.target.value)}
    />
  );
}

The parent remains the owner of the state.

A Simple Example

Let’s create two sibling components that display and update the same name.

import { useState } from "react";

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

  return (
    <div>
      <NameInput
        name={name}
        onNameChange={setName}
      />

      <NamePreview name={name} />
    </div>
  );
}

function NameInput({ name, onNameChange }) {
  return (
    <input
      value={name}
      onChange={(event) => onNameChange(event.target.value)}
      placeholder="Enter your name"
    />
  );
}

function NamePreview({ name }) {
  return (
    <h2>Hello, {name || "Guest"}!</h2>
  );
}

export default App;

The component structure is:

          App
        /     \
       ↓       ↓
 NameInput  NamePreview

The state lives in App:

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

NameInput receives the value and update function:

<NameInput
  name={name}
  onNameChange={setName}
/>

NamePreview receives the current value:

<NamePreview name={name} />

Understanding the Example Step by Step

The first step is moving the state into the parent.

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

The parent now controls the value.

Next, the parent passes the value to the input:

name={name}

The parent also passes the state updater:

onNameChange={setName}

When the user types, the child calls:

onNameChange(event.target.value)

The parent’s state changes.

React then renders the parent again.

The updated value is passed to NamePreview:

<NamePreview name={name} />

The preview automatically displays the new value.

The flow is:

User types
    ↓
NameInput
    ↓
onNameChange(value)
    ↓
App
    ↓
setName(value)
    ↓
name state changes
    ↓
NamePreview receives new name

Lifting State Up with a Temperature Converter

The classic example of lifting state up is a temperature converter.

The application contains:

          Calculator
          /        \
         ↓          ↓
 CelsiusInput   FahrenheitInput

Both inputs represent the same temperature.

The parent should own the temperature value and scale.

import { useState } from "react";

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

  const celsius =
    scale === "c"
      ? temperature
      : toCelsius(temperature);

  const fahrenheit =
    scale === "f"
      ? temperature
      : toFahrenheit(temperature);

  return (
    <div>
      <TemperatureInput
        scale="c"
        temperature={celsius}
        onTemperatureChange={(value) => {
          setScale("c");
          setTemperature(value);
        }}
      />

      <TemperatureInput
        scale="f"
        temperature={fahrenheit}
        onTemperatureChange={(value) => {
          setScale("f");
          setTemperature(value);
        }}
      />
    </div>
  );
}

function TemperatureInput({
  scale,
  temperature,
  onTemperatureChange
}) {
  const label =
    scale === "c"
      ? "Celsius"
      : "Fahrenheit";

  return (
    <fieldset>
      <legend>Enter temperature in {label}:</legend>

      <input
        value={temperature}
        onChange={(event) =>
          onTemperatureChange(event.target.value)
        }
      />
    </fieldset>
  );
}

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

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

The important concept is not the conversion formula.

The important concept is that both inputs depend on the same underlying state.

One Source of Truth

The temperature example demonstrates an important React principle:

Avoid maintaining multiple independent copies of the same state when the values are supposed to represent the same thing.

Instead of:

CelsiusInput
  temperature = ...

FahrenheitInput
  temperature = ...

the parent coordinates the value:

          Calculator
              │
       Shared temperature
          /        \
         ↓          ↓
     Celsius    Fahrenheit

This makes synchronization much easier.

Lifting State Up with a Search Box

Consider a product search page:

            App
           /   \
          ↓     ↓
     SearchBox Results

The search box changes the search term.

The results need the same search term.

The parent can own the state:

function App() {
  const [searchTerm, setSearchTerm] = useState("");

  return (
    <>
      <SearchBox
        value={searchTerm}
        onChange={setSearchTerm}
      />

      <Results searchTerm={searchTerm} />
    </>
  );
}

The search component:

function SearchBox({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={(event) => onChange(event.target.value)}
      placeholder="Search..."
    />
  );
}

The results component:

function Results({ searchTerm }) {
  return (
    <p>
      Results for: {searchTerm || "All Products"}
    </p>
  );
}

The parent coordinates both components.

Lifting State Up with a Shopping Cart

A shopping cart is another realistic example.

Consider:

              App
            /     \
           ↓       ↓
    ProductList    Cart

ProductList needs to add products.

Cart needs to display products.

The shared cart state can live in App:

function App() {
  const [cart, setCart] = useState([]);

  function addToCart(product) {
    setCart((currentCart) => [
      ...currentCart,
      product
    ]);
  }

  return (
    <>
      <ProductList onAddToCart={addToCart} />
      <Cart items={cart} />
    </>
  );
}

The product list receives the callback:

function ProductList({ onAddToCart }) {
  const product = {
    id: 1,
    name: "Wireless Mouse",
    price: 799
  };

  return (
    <button onClick={() => onAddToCart(product)}>
      Add to Cart
    </button>
  );
}

The cart receives the shared data:

function Cart({ items }) {
  return (
    <div>
      <h2>Cart</h2>

      {items.map((item) => (
        <p key={item.id}>
          {item.name} - ₹{item.price}
        </p>
      ))}
    </div>
  );
}

The state has been lifted to the common parent because both children need to participate in the same data flow.

Lifting State Up with Checkboxes

Another common example involves selecting items.

Suppose a parent renders:

       App
      /   \
     ↓     ↓
 Filters Results

The Filters component controls which category is selected.

The Results component needs to know the selected category.

The parent can own:

const [category, setCategory] = useState("All");

Then:

<Filters
  category={category}
  onCategoryChange={setCategory}
/>

<Results category={category} />

The filter component updates the parent state.

The results component receives the new value.

Lifting State Up Does Not Mean Moving Everything to App

A common misunderstanding is that lifting state up means moving all state to the root component.

It does not.

The goal is to move state to the closest common parent that needs to coordinate it.

Consider:

App
├── Header
├── Dashboard
│   ├── Search
│   └── Results
└── Footer

If only Search and Results need the search term, the state might belong in Dashboard, not necessarily in App.

App
└── Dashboard
    │
    ├── Search
    └── Results

This keeps the state closer to the components that use it.

State Ownership

A useful question when designing React components is:

Which component should own this state?

Consider:

App
├── Header
├── Profile
└── Settings

If only Settings needs a temporary form value, that state can remain inside Settings.

If both Profile and Settings need the same user preference, their common parent might need to own it.

If many unrelated components across the application need the value, Context or another state-sharing approach may be appropriate.

State ownership should be based on who needs the data.

Controlled Components and Lifting State

Lifting state up is closely connected to controlled components.

A controlled input receives its value from React state:

<input
  value={name}
  onChange={handleChange}
/>

When the state lives in the parent, the parent controls the input.

For example:

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

  return (
    <NameInput
      value={name}
      onChange={setName}
    />
  );
}

The child input becomes controlled by the parent.

This makes the current value available to other components that receive the same state.

Lifting State Up and Derived Data

Once shared state is stored in one place, other values can often be calculated from it.

For example:

function App() {
  const [items, setItems] = useState([]);

  const totalItems = items.length;

  return (
    <>
      <ItemList items={items} />
      <ItemSummary count={totalItems} />
    </>
  );
}

There is no need to maintain:

const [totalItems, setTotalItems] = useState(0);

if the count can simply be derived from items.

This reduces the number of state values that need to stay synchronized.

Lifting State Up and Immutable Updates

When lifted state contains arrays or objects, update them immutably.

For an array:

setItems((currentItems) => [
  ...currentItems,
  newItem
]);

For removing an item:

setItems((currentItems) =>
  currentItems.filter((item) => item.id !== itemId)
);

For updating an object:

setUser((currentUser) => ({
  ...currentUser,
  name: "Karim"
}));

These patterns create new values instead of directly modifying the existing state.

Common Mistakes

Lifting State Too High

Moving state all the way to the root when only two nearby components need it can make the component structure unnecessarily complicated.

Prefer the closest common parent.

Keeping Duplicate State

If two components represent the same piece of information, maintaining separate state values can cause synchronization problems.

Look for an opportunity to have one source of truth.

Mutating Shared State

Avoid:

items.push(newItem);

when items is React state.

Use:

setItems((currentItems) => [
  ...currentItems,
  newItem
]);

Forgetting to Pass the Updated Value

If a child changes state in the parent but the parent does not pass the updated state back to the components that need it, those components will not receive the new value.

The complete flow should be maintained:

Child
 ↓
Callback
 ↓
Parent state update
 ↓
New state
 ↓
Props
 ↓
Children

Using Separate State for Derived Values

Avoid storing values separately when they can be calculated from existing state.

For example:

const total = items.length;

is often preferable to maintaining another total state variable.

Using Context Too Early

Context can solve certain sharing problems, but it is not required simply because multiple components need data.

For a small number of closely related components, props and lifted state are often easier to understand.

Best Practices

Identify Who Needs the Data

Before creating state, determine which components need the value.

Find the Closest Common Parent

If multiple components need the state, look for their nearest common parent.

Keep One Source of Truth

Store the shared value in one place rather than maintaining multiple synchronized copies.

Pass Values Down Through Props

Once the parent owns the state, pass the necessary values to children.

Pass Callbacks for Updates

If children need to request changes, pass callback functions.

Keep Unrelated State Local

Not all state needs to be shared.

Keeping local state local can make components easier to understand.

Derive Data When Possible

Calculate values from existing state instead of creating additional state variables unnecessarily.

Update State Immutably

Use patterns such as:

setItems((currentItems) => [
  ...currentItems,
  newItem
]);

and:

setUser((currentUser) => ({
  ...currentUser,
  name: "Karim"
}));

A Realistic Web4Learners Example

Imagine Web4Learners has an interactive React tutorial page with a lesson sidebar and lesson content.

The component structure could be:

                  CoursePage
                 /         \
                ↓           ↓
        LessonSidebar   LessonContent

The selected lesson needs to be shared between both components.

The state can be lifted into CoursePage:

import { useState } from "react";

function CoursePage() {
  const [selectedLesson, setSelectedLesson] = useState(
    "React Introduction"
  );

  const lessons = [
    "React Introduction",
    "Components",
    "Props",
    "State",
    "Parent to Child",
    "Child to Parent",
    "Sibling Components",
    "Sharing Data Between Components",
    "Lifting State Up"
  ];

  return (
    <div>
      <LessonSidebar
        lessons={lessons}
        selectedLesson={selectedLesson}
        onSelectLesson={setSelectedLesson}
      />

      <LessonContent
        lesson={selectedLesson}
      />
    </div>
  );
}

The sidebar can request a lesson change:

function LessonSidebar({
  lessons,
  selectedLesson,
  onSelectLesson
}) {
  return (
    <aside>
      {lessons.map((lesson) => (
        <button
          key={lesson}
          onClick={() => onSelectLesson(lesson)}
          aria-current={
            lesson === selectedLesson
              ? "page"
              : undefined
          }
        >
          {lesson}
        </button>
      ))}
    </aside>
  );
}

The lesson content receives the selected lesson:

function LessonContent({ lesson }) {
  return (
    <main>
      <h1>{lesson}</h1>

      <p>
        You are currently reading the {lesson} lesson.
      </p>
    </main>
  );
}

The important part is where the state lives:

              CoursePage
                  │
           selectedLesson
              /       \
             ↓         ↓
     LessonSidebar   LessonContent
          │
          │ callback
          ↓
     setSelectedLesson()

The sidebar does not need to know how the lesson content works.

The content does not need to know how the sidebar works.

The parent coordinates both components through shared state.

This is exactly the kind of architecture that can be useful for an interactive learning platform such as Web4Learners.

Image Placement Instruction

Image placement: Add an educational diagram immediately after the section “The Basic Pattern”.

The image should visually explain the concept of lifting state up.

Show:

                 Parent
            ┌──────────────┐
            │ Shared State │
            │   value      │
            └──────┬───────┘
                   │
          ┌────────┴────────┐
          ↓                 ↓
     Child A            Child B
   updates value       receives value
          │                 ↑
          └── callback ─────┘

Use a modern Web4Learners visual style with rounded cards, subtle gradients, clean typography, clear arrows, and minimal text. The visual should make the idea understandable to a beginner without requiring the reader to study the code.

Second image placement: Add a visual diagram immediately after the section “One Source of Truth”.

Show a comparison:

❌ Separate State

Component A → value A
Component B → value B

Possible inconsistency


✓ Shared State

        Parent
          │
      shared value
       /        \
      ↓          ↓
 Component A  Component B

Use a modern, clean educational layout. Keep the image focused on the difference between duplicated state and a single shared source of truth.

Practice Exercise

Create a React application with this structure:

App
├── NameForm
└── NameDisplay

The App component should own the state:

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

NameForm should contain an input field.

When the user types:

Karim

the parent state should be updated.

NameDisplay should receive the name through props and display:

Hello, Karim!

Challenge

Create a small product filtering application:

App
├── Filter
└── ProductList

The parent should own the selected category.

The Filter component should allow the user to choose:

  • All
  • Laptops
  • Phones
  • Accessories

The ProductList should display products matching the selected category.

The important requirement is that the category state must be stored in the common parent rather than separately inside Filter and ProductList.

Extra Challenge

Add a product count:

Showing 4 products

Calculate the count from the filtered product list instead of storing the count as separate state.

Cheat Sheet

ConceptPattern
MeaningMove shared state to a common parent
When to useMultiple components need the same state
State ownerClosest common parent
Send state downProps
Request state updateCallback prop
Single source of truthOne shared state value
Child updates parentonChange(value)
Parent updates statesetValue(value)
Updated value to child<Child value={value} />
Shared arrayStore array in common parent
Add to arraysetItems([...currentItems, item])
Remove from arraysetItems(currentItems.filter(...))
Derived valueCalculate from existing state
Deep sharingConsider Context
AvoidDuplicate state and direct mutation

Key Takeaways

  • Lifting state up means moving state to the closest common parent of the components that need it.
  • It is useful when multiple components need to access or coordinate the same state.
  • The parent becomes the single source of truth.
  • The parent passes state values to children through props.
  • The parent can pass callback functions so children can request state changes.
  • Lifting state up helps keep sibling components synchronized.
  • You should not automatically move all state to the root component.
  • Keep state as close as practical to the components that need it.
  • Avoid maintaining multiple copies of the same state when they represent the same information.
  • Derived values should often be calculated from existing state rather than stored separately.
  • Update shared arrays and objects immutably.
  • Props, callback functions, and lifted state work together to create predictable component communication.
  • When shared data needs to reach many components across different levels, Context may be appropriate.
  • Lifting state up is one of the core techniques for building well-organized React applications.

Leave a Comment