Lifting State Up and Component Composition in React

React.js Full Course: Beginner to Advanced · Hooks and Data · 8 min read · Updated

One source of truth

  1. Two components need the same number, so we move the state to their closest shared parent.
  2. The parent passes the value and a change function down to the input.
  3. The person types. The input calls onChange, and the parent changes its state.
  4. The parent passes the same value to the display, which calculates Fahrenheit from it.
  5. Both parts show the same data, because there is only one copy. Data goes down, events go up.

As an app grows, you meet a new question: where should this state live? Two components far apart might need the same data. This lesson shows the simple rule that answers most of these questions, and a second idea, composition, that keeps your code clean.

The problem: two components, one value

Imagine a page with a temperature input in Celsius and a display of the same temperature in Fahrenheit. Each is a separate component. They both need the same number. If each keeps its own state, they will disagree. The numbers must come from one place.

Lifting state up

The rule: move the state to the closest parent that is shared by the components that need it. The parent keeps the state and passes the value down with props. When a child wants to change it, the parent also passes down a function.

function TemperatureApp() {
  const [celsius, setCelsius] = useState(20);

  return (
    <div>
      <CelsiusInput value={celsius} onChange={setCelsius} />
      <FahrenheitDisplay celsius={celsius} />
    </div>
  );
}

function CelsiusInput({ value, onChange }) {
  return (
    <label>
      Celsius
      <input type="number" value={value} onChange={(e) => onChange(Number(e.target.value))} />
    </label>
  );
}

function FahrenheitDisplay({ celsius }) {
  const fahrenheit = (celsius * 9) / 5 + 32;
  return <p>That is {fahrenheit.toFixed(1)} degrees Fahrenheit.</p>;
}

The state lives in TemperatureApp. CelsiusInput has no state of its own: it shows the value it is given, and calls onChange when the person types. FahrenheitDisplay calculates its answer from the same number. Because there is only one copy of the data, the two parts can never disagree. This is called a single source of truth.

Also notice that fahrenheit is not state. It is calculated from celsius. Do not store in state what you can calculate.

Data down, events up

This is the basic rhythm of React:

  • Data goes down: parents pass values to children as props.
  • Events go up: children call functions given by the parent, and the parent changes its state.

It keeps the app easy to follow. When a value looks wrong, you look at the component that owns the state. There is only one place to check.

How high should state go?

Not too high, not too low. Keep state as close to where it is used as you can, and lift it only when two or more components really need it. If you put everything at the top, every small change redraws a lot and the code is hard to read. A good test: ask “who needs to know this?”, and find the lowest component that is a parent of all of them.

The problem of passing props down many levels

Sometimes the component that needs the data is deep in the tree. You end up passing a prop through many components that do not use it, only to hand it on. People call this prop drilling.

<Page user={user}>
  <Header user={user}>
    <Menu user={user}>
      <Avatar user={user} />   {/* only Avatar uses it */}
    </Menu>
  </Header>
</Page>

For two or three levels this is fine and clear. For many levels there are two good fixes. The first is composition, shown next. The second is context, which lesson 16 covers.

Composition: pass components as children

Instead of passing data down through the middle layers, build the part that needs the data at the top, and pass it as children or as a prop. The middle layers then do not need to know about it.

function Page({ user }) {
  return (
    <Layout
      header={<Header avatar={<Avatar user={user} />} />}
    />
  );
}

function Layout({ header }) {
  return <div>{header}</div>;
}

function Header({ avatar }) {
  return <nav>{avatar}</nav>;
}

Page has the data and builds Avatar itself. Layout and Header only place whatever they are given. No prop drilling, and these components become more reusable, because they can hold anything.

This idea, composition over configuration, is a very React way to think. Instead of a component with many options (showIcon, iconPosition, iconColor), give it a place where you can put anything:

function Panel({ title, actions, children }) {
  return (
    <section>
      <header>
        <h2>{title}</h2>
        <div>{actions}</div>
      </header>
      {children}
    </section>
  );
}

<Panel title="Settings" actions={<button>Save</button>}>
  <p>Choose your options below.</p>
</Panel>

Moving state back down

Sometimes a part of the state is only used by one child, but was lifted to a parent too early. Moving it back down makes the parent simpler. Example: the text in a search box is used only by the search box, until you press Search. Keep the text inside the box, and only send the final text up on submit.

Derived data and a single source of truth

  • Keep minimal state: the smallest set of values that describes the situation.
  • Calculate everything else while drawing: totals, filtered lists, counts, labels.
  • Never keep the same fact in two places. Copies drift apart. If two components need it, they get it from one owner.
// Bad: two copies that can get out of step
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);

// Good: calculate the count
const [items, setItems] = useState([]);
const count = items.length;

A worked example: a shopping page

A shop page has a product list and a cart summary in the header. Both depend on the same cart. The page component owns the cart state, passes the cart to the header (to show the count), and passes an onAdd function to each product. When someone clicks Add on a product, the product calls onAdd(product). The page adds the product to the cart with a new array. React draws again, and the header shows the new count. No component keeps its own copy of the cart. You will build this in the task below.

Common mistakes

  • Keeping the same value in two components, so they show different numbers.
  • Lifting state too high, so the whole page redraws for a small change.
  • Forgetting to pass the handler down, so the child cannot change the parent.
  • Storing derived data in state, such as a count or a filtered list.
  • Passing the setter itself to deep children everywhere instead of a clear onSomething function.

A quick way to decide where state lives

When you are unsure, ask these three questions in order. They give a clear answer most of the time.

  1. Which components use this data? Write them down, even if there is only one.
  2. What is their closest common parent? Look at your component tree and find the lowest component that contains all of them.
  3. Is that parent too far up? If the answer is a very high component, check whether you can restructure with composition, or whether the data is really shared app-wide, which suits context.

If only one component uses the data, keep the state inside it. If two siblings use it, put it in their parent. If many components in different branches use it, think about context. Following these steps stops two common problems: state kept too high, so the whole page redraws, and state copied in two places, so the numbers disagree.

Your task

  1. Build the temperature example above and add a Kelvin display.
  2. Build a small shop: a list of three products, each with an Add button, and a header showing how many items are in the cart. Keep the cart state in the page.
  3. Add a Panel component with title, actions and children, and use it twice.

How to check it worked

  • All displays change together when the input changes.
  • The cart count in the header always matches the items added.
  • No state is stored that could be calculated.

Summary

  • When two components need the same state, move it to their closest shared parent. This keeps one source of truth.
  • Data goes down with props. Events go up with function props.
  • Keep state minimal and calculate the rest while drawing.
  • Use composition (children and component props) to avoid long chains of props.

Frequently asked questions

What does lifting state up mean?

Moving the state from a child to the closest parent that is shared by all the components that need it, and passing it down with props.

What is prop drilling?

Passing props through many levels of components that do not use them, only to reach a deep child. Composition or context can fix it.

When should I use composition instead of context?

Try composition first. Pass components as children or props from the top. Use context when many components at different depths really need the same data.

Where should state live?

As low as possible, but high enough that every component that needs it can reach it through a shared parent.