← Back to Redux
Lesson 6 · State Management Fundamentals

Lifting State Up

When two components need the same state, move it up to their closest common parent - value down, changes up. Then see, with counted re-renders, what happens when state climbs too high, and three ways back.

Beginner35 min

What you will be able to do

  • Recognise when two components need to share state
  • Lift state in three steps: remove it from the child, add it to the closest common parent, pass value down and a callback up
  • Explain "value down, changes up" - one-way data flow with callbacks
  • Find the closest common parent of two components
  • Measure what happens when state is lifted higher than needed
  • Choose a fix when lifting stops scaling: move it down, memo, or a store
  • Store what the user typed and derive the rest, so lifted inputs stay correct

The idea, in plain English

In Lesson 1 you fixed the "two copies drift apart" bug by keeping one copy in a parent and passing it down. That move has a name: lifting state up. It is the first answer React gives to shared state, it needs no library, and it is the right answer more often than people think.

The idea is simple. When two components need the same piece of state, neither of them should own it. Move it up to the closest component that contains both - their closest common parent. The parent keeps the state, passes the value down as a prop, and passes a function down so the children can ask for changes. Data still flows in one direction: values go down, change requests come up.

Lifting has one weakness. The owner re-renders every time the state changes, and by default React re-renders everything below the owner. If the state climbs higher than it needs to - all the way to App, say - every key press re-renders screens that have nothing to do with it. That is the example in the course outline, and in this lesson we count those re-renders.

All code was run with React 19.3, react-redux 9.3.0 and Redux Toolkit 2.13.0, rendered in a test browser (jsdom). We typed into the inputs and counted every render.

Worked example: State that climbed so high every screen re-renders: a search box’s text lifted to App, re-rendering 20 unrelated widgets on every key.

workflowState climbs as more components need itstep 1 / 4

1 - Local: only SearchBox has the text

The text lives in SearchBox’s useState. It works for the box - but CourseList cannot see it, so it shows all five courses whatever you type.

owner
SearchBox
typed
"re"
list shows
all 5 courses
renders (2 keys)
2

A search page, step by step. Each time a new component needs the search text, the state moves up - and more of the app re-renders when you type.

ArchitectureValue down, changes uptap a node to trace it

The parent owns the state. Children get the value as a prop. To change it, a child calls the function the parent gave it.

The problem: two siblings need the same data

A search page has two components side by side: SearchBox, where you type, and CourseList, which should show only the matching courses. In the first version, the text lives in SearchBox’s own useState - local state, as Lesson 2 recommends starting.

It does not work. We typed "re", and the list still showed all five courses. CourseList has no way to read another component’s useState. In React, a component can only receive data from above, through props. Two siblings cannot talk to each other directly.

Before.jsx - the list cannot see the text
import { useState } from "react"; const COURSES = ["Python", "Redux", "React", "Docker", "Pandas"]; function SearchBox() { const [query, setQuery] = useState(""); // only SearchBox knows the text return <input value={query} onChange={(e) => setQuery(e.target.value)} />; } function CourseList() { // CourseList needs the text to filter - but it cannot see it return <ul>{COURSES.map((c) => <li key={c}>{c}</li>)}</ul>; } export default function SearchPage() { return ( <section> <SearchBox /> <CourseList /> </section> ); }
Typed "re" - measured
list shows: Python, Redux, React, Docker, Pandas <- not filtered

An everyday picture: one guest count on the kitchen board

Two cooks in a restaurant kitchen each keep their own count of how many guests are coming. One cook hears that two more guests arrived; the other does not. Now one cooks for 12 and the other for 10.

The fix is not for the cooks to keep chasing each other. The head chef, who manages both of them, writes the guest count on the kitchen board. Both cooks read the board. When a cook learns about new guests, they do not change the board themselves - they tell the head chef, and the head chef updates it.

That is lifting state up. The head chef is the closest common parent. The board is the state. Reading the board is "value down"; telling the head chef is "changes up".

Lifting state in three steps

Step 1: remove the state from the child. SearchBox no longer has useState. Step 2: add it to the closest common parent. SearchPage now has const [query, setQuery] = useState(""). Step 3: pass it down. Both children get query as a prop; SearchBox also gets onQueryChange, a function it calls when the user types.

We typed "re" again. This time the list showed only Redux and React. SearchBox no longer decides what the text is - it shows the value it is given and reports changes. A component like this, whose value comes from its parent, is called a controlled component.

After.jsx - SearchPage owns the text
import { useState } from "react"; const COURSES = ["Python", "Redux", "React", "Docker", "Pandas"]; function SearchBox({ query, onQueryChange }) { // receives the value and a way to change it return <input value={query} onChange={(e) => onQueryChange(e.target.value)} />; } function CourseList({ query }) { // receives the value const shown = COURSES.filter((c) => c.toLowerCase().includes(query.toLowerCase())); return <ul>{shown.map((c) => <li key={c}>{c}</li>)}</ul>; } export default function SearchPage() { const [query, setQuery] = useState(""); // lifted: the closest common parent owns it return ( <section> <SearchBox query={query} onQueryChange={setQuery} /> <CourseList query={query} /> </section> ); }
Typed "re" - measured
list shows: Redux, React renders while typing 2 keys: SearchPage 2, SearchBox 2, CourseList 2 (total 6)
The three steps
1. Remove it from the childDelete useState from SearchBox.
2. Add it to the closest common parentconst [query, setQuery] = useState("") in SearchPage.
3. Pass value down, callback downquery to both children; onQueryChange={setQuery} to SearchBox.

Value down, changes up

Look at the second diagram. Data in React flows in one direction: from parent to child, as props. Lifting state does not break that rule. The value still only goes down.

So how does SearchBox change the text? The parent gives it a function - onQueryChange - and SearchBox calls it with the new text. The parent decides what to do with it (here, store it with setQuery), re-renders, and sends the new value down to both children. The child asks; the owner decides. This is the same idea as Redux’s "dispatch an action" from Lesson 1, in a smaller form.

A naming habit that helps: call the prop onSomethingChange (or onSelect, onAdd). Then anyone reading <SearchBox query={query} onQueryChange={setQuery} /> can see at once which value comes down and which function goes up.

Finding the closest common parent

Start at each component that needs the state, and walk up the tree towards App. The first component that both paths reach is the closest common parent. That is where the state should live - not higher.

Lifting higher than that is not "safer". As the next topic shows, every level you go up makes more of the app re-render on each change, and makes more components pass the value along (Lesson 5).

Where to lift
SearchBox + CourseListBoth are inside SearchPage -> lift to SearchPage.
SearchBox + HeaderTheir paths only meet at App -> lift to App (and see the fixes below).
Celsius input + Fahrenheit inputBoth inside Converter -> lift to Converter (the challenge).
Two cards in the same listBoth inside the list -> lift to the list, e.g. "which card is open".

Where it stops scaling: state that climbed too high

This is the example from the course outline. The search text is the same, but now it lives in App. App also renders a Header, a Dashboard with 20 widgets, and a Footer. None of them use the search text.

We typed the same two keys. The screen looked exactly the same as before - but the number of renders went from 6 to 54. Every key press re-rendered App, and React re-rendered everything below App: the Header, the Dashboard, all 20 widgets (40 renders), and the Footer. In a real app, those "widgets" are charts, tables and images, and the page starts to feel slow while you type.

How does state end up this high? Usually one step at a time. One day the Header wants to show "Results for re". Another day someone lifts it "just in case". Each move is small; nobody moves it back.

TooHigh.jsx
import { useState } from "react"; const COURSES = ["Python", "Redux", "React", "Docker", "Pandas"]; function Header() { return <header>Right Tech Engineering</header>; } function Widget({ n }) { return <div>Widget {n}</div>; } function Dashboard() { return <div>{Array.from({ length: 20 }, (_, i) => <Widget key={i} n={i} />)}</div>; } function Footer() { return <footer>© 2026</footer>; } function SearchBox({ query, onQueryChange }) { return <input value={query} onChange={(e) => onQueryChange(e.target.value)} />; } function CourseList({ query }) { const shown = COURSES.filter((c) => c.toLowerCase().includes(query.toLowerCase())); return <ul>{shown.map((c) => <li key={c}>{c}</li>)}</ul>; } function SearchPage({ query, onQueryChange }) { return ( <section> <SearchBox query={query} onQueryChange={onQueryChange} /> <CourseList query={query} /> </section> ); } export default function App() { const [query, setQuery] = useState(""); // climbed all the way to the top return ( <> <Header /> <Dashboard /> <SearchPage query={query} onQueryChange={setQuery} /> <Footer /> </> ); }
Renders while typing 2 keys - measured
SearchPage owns it App owns it App - 2 Header 0 2 Dashboard 0 2 Widget (x20) 0 40 Footer 0 2 SearchPage 2 2 SearchBox 2 2 CourseList 2 2 total 6 54

Fix 1: move it back down

If nothing above SearchPage really needs the text, the best fix is to undo the climb: move useState back into SearchPage, the closest common parent. App goes back to having no state at all. We measured 6 renders again, and the code is shorter - SearchPage no longer needs props.

This is the most common fix in real projects, and the cheapest. Before reaching for any tool, ask: does every component above this one really need the value?

The only change from TooHigh.jsx
function SearchPage() { const [query, setQuery] = useState(""); // moved back down return ( <section> <SearchBox query={query} onQueryChange={setQuery} /> <CourseList query={query} /> </section> ); } export default function App() { // no state any more return ( <> <Header /> <Dashboard /> <SearchPage /> <Footer /> </> ); } // measured: 6 renders for 2 keys

Fix 2 and 3: when it really must be high

Sometimes the closest common parent really is near the top - for example, when the Header shows "Results for re" and the search box is far below it. Then moving the state down is not possible. There are two common fixes.

Fix 2, memo: wrap the components that do not use the state in memo. memo tells React: "if this component’s props did not change, skip re-rendering it". Header, Dashboard and Footer take no props, so React skipped them - and all 20 widgets below the Dashboard. We measured 8 renders: App, SearchPage, SearchBox and CourseList, twice each. memo only works when the props really stay the same; a new object or function created on every render counts as a change.

Fix 3, a store: keep the text in Redux. SearchBox and CourseList each read it with useSelector, and the store tells only them about changes. App does not hold the state, so App, SearchPage and everything else do not re-render. We measured 4 renders: SearchBox and CourseList, twice each. This is the most work to set up, and Lesson 8 helps you decide when it is worth it.

Fix 2 - TooHigh.jsx with memo on the unrelated parts
import { memo, useState } from "react"; const Header = memo(function Header() { return <header>Right Tech Engineering</header>; }); const Dashboard = memo(function Dashboard() { return <div>{Array.from({ length: 20 }, (_, i) => <Widget key={i} n={i} />)}</div>; }); const Footer = memo(function Footer() { return <footer>© 2026</footer>; }); // Widget, SearchBox, CourseList, SearchPage and App: the same as TooHigh.jsx. // App still owns the query - but memo skips Header, Dashboard and Footer. // measured: 8 renders for 2 keys (App 2, SearchPage 2, SearchBox 2, CourseList 2)
Fix 3 - WithStore.jsx
import { configureStore, createSlice } from "@reduxjs/toolkit"; import { Provider, useDispatch, useSelector } from "react-redux"; const COURSES = ["Python", "Redux", "React", "Docker", "Pandas"]; const searchSlice = createSlice({ name: "search", initialState: { query: "" }, reducers: { queryChanged(state, action) { state.query = action.payload; }, }, }); const store = configureStore({ reducer: { search: searchSlice.reducer } }); function Header() { return <header>Right Tech Engineering</header>; } function Widget({ n }) { return <div>Widget {n}</div>; } function Dashboard() { return <div>{Array.from({ length: 20 }, (_, i) => <Widget key={i} n={i} />)}</div>; } function Footer() { return <footer>© 2026</footer>; } function SearchBox() { const query = useSelector((state) => state.search.query); const dispatch = useDispatch(); return <input value={query} onChange={(e) => dispatch(searchSlice.actions.queryChanged(e.target.value))} />; } function CourseList() { const query = useSelector((state) => state.search.query); const shown = COURSES.filter((c) => c.toLowerCase().includes(query.toLowerCase())); return <ul>{shown.map((c) => <li key={c}>{c}</li>)}</ul>; } function SearchPage() { return ( <section> <SearchBox /> <CourseList /> </section> ); } export default function App() { return ( <Provider store={store}> <Header /> <Dashboard /> <SearchPage /> <Footer /> </Provider> ); } // measured: 4 renders for 2 keys (SearchBox 2, CourseList 2)
All four versions - renders for 2 keys, measured
Lifted to SearchPage6 - the closest common parent.
Lifted to App54 - every component re-renders.
App + memo8 - the unrelated parts are skipped.
Redux store4 - only the two readers re-render.

Signs that lifting has stopped scaling

Lifting state up is the right first answer. These signs tell you it is time to think about the next one - Context (Lesson 7) or a store (Lesson 8).

Warning signs
The owner is near the topTyping or clicking re-renders large parts of the app.
Long prop chainsThe value and its callback travel through many components that ignore them (Lesson 5).
A crowded ownerApp has ten useState calls for unrelated features, and ten callbacks to pass down.
Callbacks passed through callbacksA child calls onChange, which calls another onChange, three levels up.
Many readers far apartThe same state is read in the header, a page, a modal and the footer.

Common questions

Is lifting state the same as global state? No. Lifted state still belongs to one component - the closest common parent - and only its children can use it. Global state (Lesson 2) is available to the whole app.

Does lifting always cause more re-renders? Only below the owner. Lifting to SearchPage re-rendered SearchPage and its two children - 6 renders - which is nothing to worry about. The cost appears when the owner is high in the tree.

Should I wrap every component in memo, to be safe? No. memo adds a comparison on every render, and it silently stops working when a prop is a new object or function each time. Use it where you have measured a problem, as in Fix 2.

Can I lift state and still keep a local draft? Yes - a child can keep its own temporary state (for example, text being edited) and only send the final value up when the user presses Save. Then name it clearly, as Lesson 4 warned.

This lesson at a glance

Lifting state up

Move state to the closest common parent of the components that need it.

Value down

Pass the state as a prop.

<CourseList query={query} />
Changes up

Pass a function the child calls.

<SearchBox onQueryChange={setQuery} />
Controlled component

Its value comes from the parent.

<input value={query} onChange={...} />
memo

Skip re-rendering when props did not change.

const Header = memo(function Header() {...})
Store

Only subscribed components re-render.

useSelector((state) => state.search.query)

Try it yourself

The code does not change. Swap the content string and the program does something else entirely.

See it fail

“Run Before.jsx and type "py". Why does the list not change?”

Count the cost

“In TooHigh.jsx, change the Dashboard to 200 widgets. How many renders do 2 key presses cause now?”

Break memo

“In the memo version, pass style={{ color: "red" }} to Header. Does memo still skip it? Why not?”

Show it in the Header

“Make Header show "Results for <query>". Where must the state live now? Which fix would you choose?”

What usually goes wrong

Keeping shared state in one of the siblings

The other sibling cannot read it. Move it to their closest common parent.

✗ function SearchBox() { const [query, setQuery] = useState(""); ... }
✓ function SearchPage() { const [query, setQuery] = useState(""); ... }
Lifting higher than the closest common parent

Every level up re-renders more of the app on each change. We measured 54 renders instead of 6.

✗ function App() { const [query, setQuery] = useState(""); ... }
✓ function SearchPage() { const [query, setQuery] = useState(""); ... }
Letting the child change the parent’s state directly

A child cannot change its props. Give it a callback and let the owner decide.

✗ props.query = e.target.value;
✓ onQueryChange(e.target.value);
Storing both of two values that depend on each other

In a converter, store what the user typed and in which box; calculate the other. Two stored values can disagree.

✗ const [celsius, setCelsius] = useState("");
const [fahrenheit, setFahrenheit] = useState("");
✓ const [temp, setTemp] = useState({ value: "", scale: "c" });

Practice

Write these yourself before opening anything. Getting them wrong first is most of how this sticks.

1.

A ProductPage has a ColourPicker and a ProductImage. The image must show the chosen colour. Where should the chosen colour live, and what does each child receive?

Show hint

Find the closest common parent. Which child shows the value, and which one changes it?

Show solution
Answer
function ProductPage() { const [colour, setColour] = useState("black"); // the closest common parent return ( <> <ColourPicker colour={colour} onColourChange={setColour} /> <ProductImage colour={colour} /> </> ); }
2.

In TooHigh.jsx, the search text is in App, but only SearchPage, SearchBox and CourseList use it. Which fix would you choose, and why?

Show hint

Does anything above SearchPage need the text?

Show solution
Answer
Fix 1 - move it back down into SearchPage. Nothing above SearchPage uses the text, so SearchPage is the closest common parent. No memo and no store needed: 6 renders instead of 54, and App and SearchPage get simpler.
3.

Name the three steps of lifting state up.

Show hint

Remove, add, pass.

Show solution
Answer
1. Remove the state from the child. 2. Add it to the closest common parent. 3. Pass the value down as a prop, and a callback down so the child can request changes.
Coding challenge

A Celsius / Fahrenheit converter

Two inputs, one for Celsius and one for Fahrenheit. Typing in either one must update the other. In the starter each input has its own state, so they never agree. Lift the state up.

It should
  • One source of truth in the parent - not two useState values that must be kept equal
  • Typing 100 in Celsius shows 212 in Fahrenheit; typing 32 in Fahrenheit shows 0 in Celsius
  • Typing 33 in Fahrenheit must keep showing 33 in that box - the box you type in must never change under your fingers
  • One reusable TemperatureInput component used twice
Starter - the two inputs never agree
import { useState } from "react"; function CelsiusInput() { const [celsius, setCelsius] = useState(""); return <label>Celsius <input value={celsius} onChange={(e) => setCelsius(e.target.value)} /></label>; } function FahrenheitInput() { const [fahrenheit, setFahrenheit] = useState(""); return <label>Fahrenheit <input value={fahrenheit} onChange={(e) => setFahrenheit(e.target.value)} /></label>; } export default function Converter() { return ( <div> <CelsiusInput /> <FahrenheitInput /> </div> ); } // type 100 in Celsius -> C="100" F="" (not updated)
Show one solution
Solution - checked: 100 C -> 212 F; 32 F -> 0 C; 33 F stays 33 (C shows 1)
import { useState } from "react"; const toF = (c) => String(Math.round((Number(c) * 9) / 5 + 32)); const toC = (f) => String(Math.round(((Number(f) - 32) * 5) / 9)); function TemperatureInput({ label, value, onValueChange }) { return <label>{label} <input value={value} onChange={(e) => onValueChange(e.target.value)} /></label>; } export default function Converter() { // ONE source of truth in the parent: what the user typed, and in which box const [temp, setTemp] = useState({ value: "", scale: "c" }); // the other box is derived, never stored const celsius = temp.scale === "c" || temp.value === "" ? temp.value : toC(temp.value); const fahrenheit = temp.scale === "f" || temp.value === "" ? temp.value : toF(temp.value); return ( <div> <TemperatureInput label="Celsius" value={celsius} onValueChange={(value) => setTemp({ value, scale: "c" })} /> <TemperatureInput label="Fahrenheit" value={fahrenheit} onValueChange={(value) => setTemp({ value, scale: "f" })} /> </div> ); } // Why store the scale? A first attempt stored only celsius and calculated // Fahrenheit from it. Typing 33 in Fahrenheit then saved celsius = 1 // (rounded), and the Fahrenheit box changed to 34 while the user typed. // Storing exactly what the user typed, and deriving the other box, fixes it.

Key points

  • When two components need the same state, move it to their closest common parent.
  • Three steps: remove it from the child, add it to the parent, pass the value and a callback down.
  • Value down, changes up: children get props and call functions; the owner decides.
  • Lift only to the closest common parent - not higher.
  • Everything below the owner re-renders on each change: 6 renders at SearchPage, 54 at App.
  • Fixes when it climbs too high: move it back down (6), memo the unrelated parts (8), or a store (4).
  • Store what the user typed and derive the rest - two stored values can disagree.

Quick check before you move on

What does "lifting state up" mean?
Moving state from a component into the closest common parent of all the components that need it.
Why could CourseList not filter in Before.jsx?
The text was in SearchBox’s own useState. A sibling cannot read another component’s state.
How does a child change state that its parent owns?
It calls a function the parent passed down as a prop, like onQueryChange(text).
What is the closest common parent?
The first component you reach when you walk up the tree from each component that needs the state.
What happened when the search text was lifted to App?
Every key press re-rendered the whole app - 54 renders for 2 keys, instead of 6.

Interview questions

What is lifting state up in React?

Moving shared state to the closest common ancestor of the components that use it. The ancestor owns the state and passes the value down as props, plus callbacks so children can request changes - keeping one source of truth and one-way data flow.

What is inverse data flow?

Children cannot change their props, so the parent passes them functions. A child calls the function with new data, and the parent updates its state, which flows back down. Data still moves one way; requests for change travel up through callbacks.

When does lifting state up stop scaling?

When the common ancestor is high in the tree: every change re-renders a large subtree, values and callbacks are drilled through many layers, and the top component collects unrelated state. Then consider memoisation, Context, or a store.

How would you reduce re-renders caused by state at the top of the tree?

First move it down to the closest common parent if possible. Otherwise memoise the subtrees that do not depend on it, or move the state to a store so only subscribed components re-render.

How do you keep two inputs that depend on each other in sync?

Lift a single source of truth to their parent - ideally exactly what the user typed and which field they typed in - and derive the other value during render rather than storing it.

Quiz

  1. 1.

    Where should the state live if SearchBox and CourseList are both inside SearchPage?

  2. 2.

    Renders for 2 key presses: SearchPage owns it, App owns it, App + memo, Redux store?

  3. 3.

    Why did memo skip Header, Dashboard and Footer?

  4. 4.

    In the converter, why was storing only celsius not enough?

  5. 5.

    Is lifted state global state?

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...