Skip to content

Lifting state up

A text input sits next to a preview that needs to show whatever's been typed into it. Both live in the same component tree, but state declared inside one component is invisible to its neighbors, so neither the input nor the preview can read or change what the other is holding. The fix is to move that state up to the closest component both of them share, then pass the value down to each as a prop. This is called lifting state up.

Here's that pair, sharing one value:

jsx
import { useState } from 'react'

function Form() {
  const [text, setText] = useState('')

  return (
    <>
      <TextInput text={text} onChange={setText} />
      <TextDisplay text={text} />
    </>
  )
}

function TextInput({ text, onChange }) {
  return <input value={text} onChange={e => onChange(e.target.value)} />
}

function TextDisplay({ text }) {
  return <p>You typed: {text}</p>
}

Form is the closest common parent of TextInput and TextDisplay, so that's where text lives. It passes text down to both, and it passes setText down to TextInput under the name onChange. When someone types, TextInput calls onChange with the new value, Form's state updates, and Form re-renders. That re-render sends the new text back down to both children, so TextDisplay shows exactly what's in the input without holding any state of its own.

Keep state as low as it can live

Lifting state up is a move you make only when you need it. The guiding rule is state colocation: keep each piece of state as low in the tree as it can live, close to the component that actually uses it, and lift it only when something else needs to read or change it too. A component with state nobody else touches should keep that state to itself.

This matters because lifting has a cost. Every level a piece of state moves up, the parent has to pass it back down again as a prop, and every component in between has to accept and forward that prop even if it never uses the value itself. Start local. Move state up only as far as the nearest component that actually needs to share it, and no further.

Passing the setter down

Notice that Form doesn't only pass text down. It passes setText down too, as the onChange prop. This is the other half of lifting state up: the component that owns the state hands out a way to change it via props, and the child calls that function instead of managing the value itself. TextInput never sees setText directly. It calls whatever function it was given, and the parent decides what that function actually does.

For state that's shared between components nested many levels apart, passing props down through each layer stops being practical. React has tools for that case, Context among them, and the Hooks chapter covers useContext, the hook that reads a value without threading it through every component in between. That's beyond what this chapter covers; the pattern above is the one to reach for first.

Pick one component to own a given piece of state and treat it as the single source of truth for that value. Don't let a second component hold a copy of the same data in its own state and try to keep the two in sync by hand. Two copies of the same value drift apart the moment one update path is missed, and chasing that bug is worse than the extra props it would have taken to lift the state in the first place. If a value can be derived from state that already exists somewhere, derive it during render instead of storing it separately.

The signal to watch for is prop drilling: a prop that gets threaded through several components purely to reach one deeply nested consumer, with every component in between passing it straight through and never using it. A layer or two of that is normal and not worth restructuring around. When it stretches across many layers, or when several unrelated branches of the tree all need the same value, that's when Context or a state management library earns its keep. Lifting state up and passing props down is still the right tool for state shared by components that are close together; it's the wrong tool once the tree between them gets deep.

JunoMove shared state to the parent When two components need the same value, the trick is to give the value to their closest shared parent instead of either component. That parent holds the state, passes the value down to both, and passes down a function so a child can ask for changes. The child never owns the value; it uses what it's given and asks the parent to update it.
JunoMove shared state to the parent Lift state to the nearest common parent when two components need to share it, and pass the value and its setter down as props, usually a value prop and an onChange-style callback. Keep state as local as it can be otherwise; only lift when something else needs it too. For anything shared across a lot of nested components, Context is the better tool, and the Hooks chapter covers how.
JunoMove shared state to the parent Default to colocated state and lift only as far as the nearest shared consumer, passing the value and its setter down as props. Keep one source of truth per value rather than syncing copies across components. Watch for prop drilling, a prop threaded through layers that don't use it, as the signal that Context or a store is the better fit rather than lifting further.

Next up: Effects, where you'll use useEffect to handle work that happens outside of rendering.