Skip to main content

Command Palette

Search for a command to run...

Lifting State Up – Sharing State Between Components

Updated
12 min readView as Markdown

As React applications become bigger, components rarely work completely independently.

Imagine a product page:

                App
                 │
        ┌────────┴────────┐
        ↓                 ↓
   ProductInfo        CartSummary
        │                 │
        └─────── ? ───────┘

Suppose ProductInfo knows that a product has been added to the cart.

But CartSummary also needs to know.

Where should the state live?

If both components maintain their own separate state, they can easily become inconsistent.

React provides a pattern for this:

Lifting State Up

We move the shared state to their closest common parent.

              Parent
             /      \
            ↓        ↓
        Child A    Child B
            ↑        ↑
          shared state

Today we'll understand how this works and why it is one of the most important patterns in React.


🧠 What Does "Lifting State Up" Mean?

Suppose we have two components:

<TemperatureInput />
<TemperatureDisplay />

Both need to know the same temperature.

Instead of storing temperature independently in both components, we store it in their parent:

function App() {
  const [temperature, setTemperature] = useState("");

  return (
    <>
      <TemperatureInput
        temperature={temperature}
        setTemperature={setTemperature}
      />

      <TemperatureDisplay
        temperature={temperature}
      />
    </>
  );
}

The state has been lifted up from the child components into their common parent.


🤔 Why Do We Need This?

Imagine two components:

Component A
    ↓
count = 5

Component B
    ↓
count = 10

They are supposed to represent the same information.

That's a problem.

We want:

        Parent State
             ↓
       count = 5
         ↙     ↘
        ↓       ↓
   Component A Component B

Now both components receive the same source of truth.


🎯 The Single Source of Truth

One of the most important ideas behind lifting state is:

There should be one authoritative place where a particular piece of shared state is stored.

For example:

const [count, setCount] = useState(0);

The parent owns the state.

Children receive what they need through props.

Parent
 │
 ├── state
 │
 ├── ↓ value
 │
 └── ↓ event handler
      │
      ├── Child A
      └── Child B

🔄 State Flows Down

React follows one-way data flow.

If the parent has:

const [count, setCount] = useState(0);

it can pass the value down:

<Child count={count} />

The child receives:

function Child({ count }) {
  return <p>{count}</p>;
}

So:

Parent State
     ↓
Props
     ↓
Child

🔁 But How Does the Child Update Parent State?

This is where it gets interesting.

The child doesn't directly modify the parent's state.

Instead, the parent passes a function.

Parent:

function App() {
  const [count, setCount] = useState(0);

  function increaseCount() {
    setCount(prev => prev + 1);
  }

  return (
    <Child onIncrease={increaseCount} />
  );
}

Child:

function Child({ onIncrease }) {
  return (
    <button onClick={onIncrease}>
      Increase
    </button>
  );
}

The flow is:

Parent owns state
       ↓
Parent passes state
       ↓
Parent passes handler
       ↓
Child triggers handler
       ↓
Parent updates state
       ↓
Parent re-renders
       ↓
New state flows down

This is one of the most important React patterns.


🧩 Simple Example

Let's build a simple counter.

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <CounterDisplay count={count} />

      <CounterButtons
        onIncrease={() =>
          setCount(prev => prev + 1)
        }
      />
    </div>
  );
}

function CounterDisplay({ count }) {
  return <h1>{count}</h1>;
}

function CounterButtons({ onIncrease }) {
  return (
    <button onClick={onIncrease}>
      Increase
    </button>
  );
}

export default App;

Notice:

Parent owns state

const [count, setCount] = useState(0);

Display receives the value

<CounterDisplay count={count} />

Button receives the function

<CounterButtons onIncrease={...} />

The state stays in the parent.


🔥 Why Not Keep State Inside the Child?

We could do:

function CounterButtons() {
  const [count, setCount] = useState(0);
}

But now CounterDisplay doesn't know about that state.

If multiple components need the same information, keeping the state inside one child makes sharing difficult.

So we move it upward.


📦 Example: Temperature Converter

This is a classic example of lifting state.

Suppose we want:

Celsius: [ 25 ]

Fahrenheit: 77

Both values represent the same temperature.

We don't need two independent sources of truth.

We can store Celsius in the parent:

import { useState } from "react";

function App() {
  const [temperature, setTemperature] = useState("");

  const fahrenheit =
    temperature === ""
      ? ""
      : temperature * 9 / 5 + 32;

  return (
    <div>
      <TemperatureInput
        value={temperature}
        onChange={setTemperature}
      />

      <h2>
        Fahrenheit: {fahrenheit}
      </h2>
    </div>
  );
}

The state is shared through the parent.


🧮 Derived Data

Notice something important:

We didn't create:

const [fahrenheit, setFahrenheit] = useState("");

Instead:

const fahrenheit =
  temperature === ""
    ? ""
    : temperature * 9 / 5 + 32;

Why?

Because Fahrenheit is derived from Celsius.

We already have the source data.

There's no need to create another state variable for something we can calculate.

This is an important state-management principle:

Don't store data in state if it can be calculated from existing state or props.


🛒 Real-World Example: Shopping Cart

Imagine an application with:

                App
                 │
        ┌────────┴─────────┐
        ↓                  ↓
   ProductList        CartSummary

ProductList needs to add products.

CartSummary needs to display the cart.

The cart state can live in App.

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

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

  return (
    <>
      <ProductList
        onAddToCart={addToCart}
      />

      <CartSummary
        cart={cart}
      />
    </>
  );
}

Now:

                App
                 │
           cart state
           /         \
          ↓           ↓
 ProductList      CartSummary
     │                 │
     │                 │
 addToCart         cart data

This is exactly how component communication starts to work in larger applications.


🔄 Child-to-Parent Communication

Technically, React's data flow is still one-way.

The child isn't directly sending state upward.

Instead:

Parent
  ↓
passes function
  ↓
Child
  ↓
calls function
  ↓
Parent updates state

This can feel like "child-to-parent communication," but the actual mechanism is:

Parent passes a callback to the child.


📞 Callback Props

A function passed through props is often called a callback prop.

Parent:

function handleDelete(id) {
  console.log("Delete:", id);
}

<Task
  onDelete={handleDelete}
/>

Child:

function Task({ onDelete }) {
  return (
    <button onClick={() => onDelete(10)}>
      Delete
    </button>
  );
}

The child invokes:

onDelete(10);

The parent's function runs.


🧠 Passing Arguments Through Callbacks

This is extremely common.

Parent:

function handleSelect(id) {
  console.log(id);
}

Pass:

<UserList onSelect={handleSelect} />

Child:

function UserList({ onSelect }) {
  return (
    <button onClick={() => onSelect(101)}>
      Select User
    </button>
  );
}

The child doesn't need to know what the parent will do.

It simply calls the callback.


🧩 Lifting State in Forms

Yesterday we learned controlled forms.

Now let's combine that with lifting state.

Suppose we have:

App
 │
 ├── SearchInput
 │
 └── SearchResults

Both need the search query.

Instead of storing the query separately:

SearchInput
  ↓
query state

SearchResults
  ↓
query state

we lift it:

             App
              │
         query state
          /       \
         ↓         ↓
 SearchInput   SearchResults

Parent:

function App() {
  const [query, setQuery] = useState("");

  return (
    <>
      <SearchInput
        query={query}
        onQueryChange={setQuery}
      />

      <SearchResults
        query={query}
      />
    </>
  );
}

Now both components always use the same query.


🔎 Search Example

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

Results:

function SearchResults({ query }) {
  return (
    <p>
      Searching for: {query}
    </p>
  );
}

The parent owns the state.


🧠 Why This Pattern Is Powerful

Imagine a real application:

Search Bar
    ↓
Search State
    ↓
┌───────────────────────────────┐
│                               │
↓                               ↓
Search Results              Result Count

All components can stay synchronized because they depend on the same state.

Without lifting state, you might end up with:

Search Bar → query A

Results → query B

Count → query C

Now synchronization becomes difficult.


🔄 Lifting State + Conditional Rendering

We can combine today's concept with Day 65.

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <>
      <Navbar
        isLoggedIn={isLoggedIn}
        onLogout={() => setIsLoggedIn(false)}
      />

      {isLoggedIn ? (
        <Dashboard />
      ) : (
        <Login
          onLogin={() => setIsLoggedIn(true)}
        />
      )}
    </>
  );
}

The parent owns authentication state.

Children receive the data/functions they need.


🧩 Lifting State + Lists

We can also combine this with Day 66.

function App() {
  const [selectedId, setSelectedId] = useState(null);

  const users = [
    { id: 1, name: "Saurabh" },
    { id: 2, name: "Rahul" },
    { id: 3, name: "Aman" }
  ];

  return (
    <UserList
      users={users}
      selectedId={selectedId}
      onSelect={setSelectedId}
    />
  );
}

Inside:

function UserList({
  users,
  selectedId,
  onSelect
}) {
  return (
    <div>
      {users.map(user => (
        <button
          key={user.id}
          onClick={() => onSelect(user.id)}
        >
          {user.name}
        </button>
      ))}

      {selectedId && (
        <p>
          Selected User ID: {selectedId}
        </p>
      )}
    </div>
  );
}

Now we're combining:

State
+
Events
+
Props
+
Lists
+
Conditional Rendering

🚨 When Should You Lift State?

A good rule is:

Lift state when multiple components need access to the same changing data.

For example:

Component A needs state
Component B needs state
        ↓
Find common parent
        ↓
Move state there
        ↓
Pass value + handlers through props

⚠️ Don't Lift State Too Far

There is also an opposite mistake.

You don't need to move every piece of state to the highest possible component.

Suppose:

function PasswordInput() {
  const [showPassword, setShowPassword] = useState(false);
}

If only PasswordInput needs this state, keep it there.

Don't move it all the way to App unnecessarily.

Good state placement is:

Keep state as close as possible to where it's needed, but lift it when siblings need to share it.


🧠 State Placement Rule

Think about it like this:

Only one component needs it?
        ↓
Keep state there.

Multiple siblings need it?
        ↓
Lift state to their common parent.

Many distant components need it?
        ↓
You may eventually need a broader state-management solution.

We'll encounter those patterns later.


🏗️ Example: Accordion

Imagine an FAQ:

What is React?
      ↓
Answer

What is State?
      ↓
Answer

What are Props?
      ↓
Answer

If we want only one item open at a time, the parent can own:

const [activeIndex, setActiveIndex] = useState(null);

Then each FAQ item receives:

isActive
onClick

This allows the parent to control which child is active.


🧠 Controlled Components Beyond Forms

The concept we've learned today is broader than forms.

A component can be "controlled" by its parent whenever the parent owns the relevant state and passes the current value plus callbacks.

For example:

<Tabs
  activeTab={activeTab}
  onTabChange={setActiveTab}
/>

or:

<Modal
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
/>

or:

<Accordion
  activeIndex={activeIndex}
  onChange={setActiveIndex}
/>

The same pattern keeps appearing.


🏆 Best Practices

1. Find the common parent

When two components need the same state, identify their closest common parent.


2. Keep one source of truth

Don't maintain duplicate copies of the same state unnecessarily.


3. Pass data down through props

<Component value={value} />

4. Pass callbacks down for updates

<Component onChange={handleChange} />

5. Don't modify parent state directly

The child should call the callback.


6. Don't lift state unnecessarily

If only one component needs it, keep it local.


7. Avoid duplicate derived state

If something can be calculated from existing state, calculate it instead of storing another state variable.


🔥 Complete Example: Shared Counter

Let's put everything together.

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  function increase() {
    setCount(prev => prev + 1);
  }

  function decrease() {
    setCount(prev => prev - 1);
  }

  return (
    <div>
      <CounterDisplay count={count} />

      <CounterControls
        onIncrease={increase}
        onDecrease={decrease}
      />
    </div>
  );
}

function CounterDisplay({ count }) {
  return (
    <h1>
      Count: {count}
    </h1>
  );
}

function CounterControls({
  onIncrease,
  onDecrease
}) {
  return (
    <div>
      <button onClick={onIncrease}>
        +
      </button>

      <button onClick={onDecrease}>
        -
      </button>
    </div>
  );
}

export default App;

Notice how clean the responsibilities are.

App

Owns the state.

CounterDisplay

Displays the value.

CounterControls

Triggers updates.

             App
              │
          count state
           /       \
          ↓         ↓
      Display    Controls
        ↑          │
        │          │
        └──────────┘
          callbacks

This is excellent component design.


💡 My Biggest Takeaway

Today's biggest takeaway is:

When multiple components need the same state, don't create duplicate state. Lift it to their closest common parent.

The pattern is:

             Parent
                │
        ┌───────┴────────┐
        ↓                ↓
    Child A           Child B
        ↑                ↑
        └── callbacks ───┘
                │
          Parent State

And the fundamental React data flow remains:

State
 ↓
Props
 ↓
Child
 ↓
Callback
 ↓
Parent State Update
 ↓
Re-render
 ↓
Updated Props

Once you understand this, component communication becomes much easier to reason about.

2 views

100 Days of Code: My Journey to Becoming a Full Stack Developer

Part 1 of 50

Welcome to my 100 Days of Code journey! In this series, I'll document my daily progress as I learn Full Stack Web Development from the ground up. Every post will cover what I learned, challenges I faced, mistakes I made, and the projects I built. My goal is not just to complete 100 days but to become a better developer through consistency, discipline, and learning in public. Topics I'll cover include: • Git & GitHub • HTML, CSS & JavaScript • React.js • Node.js & Express • MongoDB • APIs • Real-world Projects • AI tools for Developers Whether you're just starting out or revising your fundamentals, I hope this journey helps you learn alongside me. Let's build, learn, and grow together! 🚀

More from this blog

T

TheSaurceCode

73 posts

Documenting my journey to becoming a Full Stack Developer through daily blogs, coding challenges, projects, tutorials, and lessons learned. Learn, build, and grow with me