Lifting up the state in React

December 27, 2020

Person lifting weights

Photo by Victor Freitas on Unsplash

In this post I describe lifting up the state in React basing on a simple example.

Scenario of lifting up the state

Typically, when creating components, we make them stateful because we need to hold some data which is necessary for the component to render. Let’s use an example of a ButtonDisplayingClicks component that shows how many times the button was clicked:

class ButtonDisplayingClicks extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0,
    };
  }

  handleClick = () => {
    this.setState(prevState => ({
      counter: prevState.counter + 1,
    }));
  };

  render() {
    return (
      <button onClick={this.handleClick}>
        Clicked {this.state.counter} times
      </button>
    );
  }
}

ReactDOM.render(<ButtonDisplayingClicks />, document.getElementById("root"));

We hold the information about clicks in the counter state property and display the value in the message inside the button.

Now let’s imagine that we want to refactor the ButtonDisplayingClicks component and have the button and display message as a two standalone separate components. As a first step we could create new Button and Display components and simply divide the existing code between them. Then we could use these components inside the new parent ClickCounter component, which could look like this:

function ClickCounter(props) {
  return (
    <>
      <Button />
      <Display />
    </>
  );
}

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0,
    };
  }

  handleClick = () => {
    this.setState(prevState => ({
      counter: prevState.counter + 1,
    }));
  };

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

function Display(props) {
  return <p>Clicked {counter} times</p>;
}

ReactDOM.render(<ClickCounter />, document.getElementById("root"));

But this code will not work, because the Display button does not have access to the counter property of the Button component’s state. As I mentioned in this React state basics post, the state is private to the component that holds it and cannot be accessed or modified from outside. So now we have a situation in which there are two separate components that depends on the same state - a Display that needs to show the counter, and Button that needs to update it upon being clicked. To solve the problem, we can lift up the state. The idea is to move the state that needs to be accessed by two or more components up to their closest common ancestor. Then we can use props for passing the state property values and update handler methods to children components.

Since in our example the closest ancestor of both Button and Display components is the ClickCounter component, we need to lift (move) the state with the counter property up to this component. The sketch below visualizes this:

Drawing visualising lifting the state up

Lifting up the state from Button to ClickCounter

Here is how the final code looks like:

class ClickCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0,
    };
  }

  incrementCounter = () => {
    this.setState(prevState => ({
      counter: prevState.counter + 1,
    }));
  };

  render() {
    return (
      <>
        <Button onClick={this.incrementCounter} />
        <Display message={`Clicked ${this.state.counter} times`} />
      </>
    );
  }
}

function Button(props) {
  return <button onClick={props.onClick}>Click me</button>;
}

function Display(props) {
  return <p>{props.message}</p>;
}

The state is lifted up to the ClickCounter component and we pass down the counter property value down to the Display component within the message prop. Since the component’s state can only be updated from within this component, the ClickCounter component also contains the method for updating the state which was previously inside the Button component (it is renamed to incrementCounter since this name is more appropriate from the parent’s point of view). Since now the Button component does not have any methods, we can simplify it an make it a function component (instead of class component). The last important part is updating the state when the button is clicked. A component cannot modify its parent state directly, therefore we pass the incrementCounter handler method within the onClick prop, and invoke it when button’s onClick event is fired, updating state as a result.

Summary

The important message is that state can be lifted up when there are two or more separate components depending on it. We should lift only the parts of the state that are relevant. Then we can pass the state down to the children through props. To update the state, we need to additionally pass down the parents’ handler methods through props, and invoke them when appropriate event happens.


Written by Marcin Kapica