Sharing State Between Components

Sometimes, you want the state of two components to always change together. To do it, remove state from both of them, move it to their closest common parent, and then pass it down to them via props. This is known as “lifting state up”, and it’s one of the most common things you will do writing React code.

You will learn

  • How to share state between components by lifting it up
  • What are controlled and uncontrolled components

Lifting state up by example

In this example, there are two components.

There is a parent Accordion component that renders two separate Panels. Each Panel component has a boolean isActive state that determines whether its content is visible.

Press the Show button for both panels:

import { useState } from 'react';

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}

export default function Accordion() {
  return (
    <>
      <Panel title="Ingredients">
        Milk, tea bags, and a cinnamon stick.
      </Panel>
      <Panel title="Recipe">
        Heat the milk and put tea bags into the pan.
        Add the cinnamon stick.
      </Panel>
    </>
  );
}

Notice how pressing one panel’s button does not affect the other panel—they are independent.

But now let’s say you want to change it so that only one panel is expanded at any given time. With that design, expanding the second panel should collapse the first one. How would you do that?

To coordinate these two panels, you need to “lift their state up” to a parent component in three steps

On the left are two components each owning their own state values. On the right, they are the children of a parent component that owns both their state values.

Step 1: Remove state from the child components

You will give control of the Panel’s isActive to a parent component of both Panels. Then you’ll pass the state back to the children as a prop. Start by removing this line from the Panel component:

const [isActive, setIsActive] = useState(false);

And instead, add isActive to the Panel’s list of props:

function Panel({ title, children, isActive }) {

Now the Panel’s parent component can control isActive by passing it down as a prop. Conversely, the Panel component now has no control over the value of isActive—it’s now up to the parent component!

Step 2: Pass hardcoded data from the common parent

To lift state up, you must locate the closest common parent component of both of the child components that you want to coordinate:

  • Accordion (closest common parent)
    • Panel
    • Panel

In this example, it’s the Accordion component. Since it’s above both panels and can control their props, it will become the “source of truth” for which panel is currently active. Make the Accordion component pass a hardcoded value of isActive (for example, true) to both panels:

import { useState } from 'react';

export default function Accordion() {
  return (
    <>
      <Panel title="Ingredients" isActive={true}>
        Milk, tea bags, and a cinnamon stick.
      </Panel>
      <Panel title="Recipe" isActive={true}>
        Heat the milk and put tea bags into the pan.
        Add the cinnamon stick.
      </Panel>
    </>
  );
}

function Panel({ title, children, isActive }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}

Try editing the hardcoded isActive values in the Accordion component and see the result on the screen.

Step 3: Add state to the common parent

Lifting state up often changes the nature of what you’re storing as state. In this case, only one panel should be active at a time. This means that the Accordion common parent component needs to keep track of which panel is the active one. Instead of a boolean value, it could use an number as the index of the active Panel for the state variable:

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

When the activeIndex is 0, the first panel is active, and when it’s 1, it’s the second one.

Clicking the “Show” button in either Panel needs to change the active index in Accordion. A Panel can’t set the activeIndex state directly because it’s defined inside the Accordion. The Accordion component needs to explicitly allow the Panel component to change its state by passing an event handler down as a prop:

<Panel
  isActive={activeIndex === 0}
  onShow={() => setActiveIndex(0)}
>
  ...
</Panel>
<Panel
  isActive={activeIndex === 1}
  onShow={() => setActiveIndex(1)}
>
  ...
</Panel>

The <button> inside the Panel will now use the onShow prop as its click event handler:

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <Panel
        title="Ingredients"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        Milk, tea bags, and a cinnamon stick.
      </Panel>
      <Panel
        title="Recipe"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        Heat the milk and put tea bags into the pan.
        Add the cinnamon stick.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Show
        </button>
      )}
    </section>
  );
}

This completes lifting state up! Moving state into the common parent component allowed you to coordinate the two panels. Using the active index instead of two “is shown” flags ensured that only one panel is active at a given time. And passing down the event handler to the child allowed the child to change the parent’s state.

Deep Dive

Controlled and Uncontrolled Components

The parent component passes the state setting function to both child components.

A single source of truth for each state

In a React application, many components will have their own state. Some state may “live” close to the leaf components like inputs. Other state may “live” closer to the top of the app. For example, even a router is usually implemented by storing the current route in the React state, and passing it down by props!

For each unique piece of state, you will choose the component that “owns” it. This principle is also known as having a “single source of truth.” It doesn’t mean that all state lives in one place—but that for each piece of state, there is a specific component that holds that information. Instead of duplicating shared state between components, you will lift it up to their common shared parent, and pass it down to the children that need it.

Your app will change as you work on it. It is common that you will move state down or back up while you’re still figuring out where each piece of the state “lives”. This is all part of the process!

Recap

  • When you want to coordinate two components, move their state to their common parent.
  • Then pass the information down through props from their common parent.
  • Finally, pass the event handlers down so that the children can change the parent’s state.
  • It’s useful to consider components as “controlled” (driven by props) or “uncontrolled” (driven by state).

Challenge 1 of 2:
Synced inputs

These two inputs are independent. Make them stay in sync: editing one input should update the other input with the same text, and vice versa. You’ll need to lift their state up into the parent component.

import { useState } from 'react';

export default function SyncedInputs() {
  return (
    <>
      <Input label="First input" />
      <Input label="Second input" />
    </>
  );
}

function Input({ label }) {
  const [text, setText] = useState('');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <label>
      {label}
      {' '}
      <input
        value={text}
        onChange={handleChange}
      />
    </label>
  );
}