DEV Community

Cover image for Things you need to know about React state
Romain Trotard
Romain Trotard

Posted on • Edited on • Originally published at romaintrotard.com

Things you need to know about React state

You probably know what is a React state and the difference with props. But do you know everything about it?

In this article, we will see how to use state from Class component to Functional component, things to take care, tips...

What is it?

You probably want to add interactivity on your page and more particularily on your React component. This interaction will maybe change the UI, in this case store data into a React state and change the rendering in function of it is the way to go.

Unlike props, a component can change its state. But there are some rules to follow to have a re-rendering of your component when changing state. Let's see it.

Class component

Before going into what you know nowadays i.e. hooks. It was a time where hooks did not exist and the only way to have a stateful component was to use Component class.

Note: It's in the version 16.8.6 of React that hooks has been exposed by the library.

The way to make a Component class was to create a class and extends the React.Component class, then you have access to life-cycle methods:

  • constructor
  • componentDidMount
  • componentDidUpdate
  • render (required)
  • componentWillUnmount
import React from "react";

class MyClassComponent extends React.Component {
  render() {
    return <p>A simple class component</p>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Initialization

Then you can initialize its state in two different ways:

  • in constructor
class MyClassComponent extends React.Component {
  constructor() {
    this.state = {
      firstName: "Bob",
      lastName: "TheSponge",
    };
  }
  render() {
    return <p>A simple class component with a state</p>;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • declaring the property state directly
class MyClassComponent extends React.Component {
  state = {
    firstName: "Bob",
    lastName: "TheSponge",
  };

  render() {
    return <p>A simple class component with a state</p>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: You can use both as you wish. It's the same. But constructor will give you the possibility to use props to initialize the state.


Access the state

As you can probably imagine you can now access the state by simply using this.state:

class MyClassComponent extends React.Component {
  state = {
    firstName: "Bob",
    lastName: "TheSponge",
  };

  render() {
    return (
      <div>
        <p>First name: {this.state.firstName}</p>
        <p>Last name: {this.state.lastName}</p>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

State update

If you have a state that you never update, it's probably you don't need a state to store this data.
To update, the state you have access to a method setState from the component instance this.
You can then change anything in the state.

Things to know about setState

Unlike in component class with useState, setState will merge the updated data with the prev one automatically:

class MyClassComponent extends React.Component {
  state = {
    firstName: "Bob",
    lastName: "TheSponge",
  };

  updateFirstName = () => {
    // It will result having a state with
    // { firstName: 'New firstName', lastName: 'TheSponge' }
    this.setState({ firstName: "New firstName" });
  };

  render() {
    const { firstName, lastName } = this.state;

    return (
      <div>
        <p>First name: {firstName}</p>
        <p>Last name: {lastName}</p>
        <button
          type="button"
          onClick={this.updateFirstName}
        >
          Update firstName
        </button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Warning: It will not deeply merge. Just shallow stuff. If you need to update a single key of an object store in a state, you will have to update it in function of the previous object stored.


Update state in function of the previous one

Like said in the warning above, when you want to :

  • update a part of an object stored in a state
  • just update the state in function of the previous one (for example for a counter)

Then you will use another API of the setState function.
Yep setState can be used with two different ways:

  • passing the new state
  • passing a callback with as parameter the previous state and return the new one
class MyClassComponent extends React.Component {
  state = {
    counter: 0,
  };

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

  render() {
    return (
      <button type="button" onClick={this.incrementCounter}>
        Increment: {this.state.counter}
      </button>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: In this case state will be "shallowly" merged too.

You may tell yourself: It's overkill to do like that, because I have access to the previous counter with this.state.counter

Yep you are right. But when you :

  • update same property of the state multiple times in a row:
class MyClassComponent extends React.Component {
  state = {
    counter: 0,
  };

  // This will only increment by 1 because when calling the
  // the value of `this.state.counter` is 0
  // for all 3 `setState`
  incrementByThreeCounter = () => {
    this.setState({
      counter: this.state.counter + 1,
    });
    this.setState({
      counter: this.state.counter + 1,
    });
    this.setState({
      counter: this.state.counter + 1,
    });
  };

  render() {
    return (
      <button
        type="button"
        onClick={this.incrementByThreeCounter}
      >
        Increment: {this.state.counter}
      </button>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
  • work with asynchronous stuff
class FoodOrdering extends React.Component {
  state = {
    orderInProgressCount: 0,
    orderDeliveredCount: 0,
  };

  order = async () => {
    // I tell myself that I can destructure
    // `loading` from the state because it used at multiple place
    // but it's a bad idea
    const { orderInProgressCount, orderDeliveredCount } =
      this.state;
    this.setState({
      orderInProgressCount: orderInProgressCount + 1,
    });
    await fakeAPI();
    // In this case `loading` is still false
    this.setState({
      orderInProgressCount: orderInProgressCount - 1,
      orderDeliveredCount: orderDeliveredCount + 1,
    });
  };

  render() {
    const { orderInProgressCount, orderDeliveredCount } =
      this.state;

    return (
      <div>
        <p>Order in progress: {orderInProgressCount}</p>
        <p>Order delivered: {orderDeliveredCount}</p>
        <button type="button" onClick={this.order}>
          Order food
        </button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Play with it here:

So I recommend you the callback API when you need the previous value, not to have some suprise.

We have played enough with Component classes, now let's see how to use a state in a Functional components.


Functional component

From the version 16.8.6, it is possible to do stateful Functional component thanks to the useState hooks. Let check together how to use it.

Initialization

The initial value of the state is given as parameter to the useState hook. There are 2 ways to do it:

  • giving the value directly
import { useState } from "react";

function StateFunctionalComponent() {
  // The initial value is 0
  useState(0);

  return <p>Functional component with state</p>;
}
Enter fullscreen mode Exit fullscreen mode
  • giving a callback to do a lazy initialization
import { useState } from "react";

function initializeState() {
  return 0;
}

function StateFunctionalComponent() {
  // The initial value will be
  // initialized in a lazy way to 0
  useState(initializeState);

  return <p>Functional component with state</p>;
}
Enter fullscreen mode Exit fullscreen mode

What is the difference between the following initialization for you?

useState(initializeState());
Enter fullscreen mode Exit fullscreen mode

And

useState(initializeState);
Enter fullscreen mode Exit fullscreen mode

Not obvious, right?

In fact in the first code the initializeState will be called at every render unlike the second one that will be called only at the first render.

It can be interesting to use lazy initialization when you have a process with high performance.

How to access the state

To know how to access we have to see what's the useState returns.
It will return an array, with the value as first element and the updater as second element:

const [value, setValue] = useState('Initial value');
Enter fullscreen mode Exit fullscreen mode

So then I just have to use the value.

Note: You can name the value and the updater with the name you want, because there is a destructuring of the array :)

const [counter, setCounter] = useState(0);
Enter fullscreen mode Exit fullscreen mode

Update the state

Then, to update the state, you just have to use the updater. Like with *Component class there is 2 ways to do it:

  • passing a value directly
function Counter() {
  const [counter, setCounter] = useState(0);

  return (
    <button type="button" onClick={() => setCounter(100)}>
      Change counter: {counter}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • passing a callback which will give you access to the previous value of the state:
function Counter() {
  const [counter, setCounter] = useState(0);

  return (
    <button
      type="button"
      onClick={() => setCounter((prev) => prev + 1)}
    >
      Increment counter: {counter}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

For the same reason that I described in the Component class part, I recommend to use the callback API when you need the previous value.


Things to know about state in Functional Component

No merge done automatically

When you update a state in a Function component, there is no merge of the state. So if your state has an object it will remove all key that you do not pass during the update:

function Person() {
  const [person, setPerson] = useState({
    firstName: "Bob",
    lastName: "TheSponge",
  });

  const updateFirstName = () => {
    // When doing that you will lose the lastName key
    // in your person object
    setPerson({ firstName: "Romain" });
  };

  return (
    <div>
      <p>First name: {firstName}</p>
      <p>Last name: {lastName}</p>
      <button type="button" onClick={updateFirstName}>
        Update firstName
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Store a function a in a state

Because, the APIs of useState can take a callback during initialization and when updating the state. If you want to store a function, you will have to use the callback API during both, otherwise your function will be executed and the returned value will be stored:

function firstFunction() {
  // Do some stuff
  return "Hello";
}

function secondFunction() {
  // Do some stuff
  return "Guys and girls";
}

export default function MyComponent() {
  // If you do `useState(firstFunction)`
  // It will be 'Hello' that will be stored
  const [myFunction, setMyFunction] = useState(
    () => firstFunction
  );

  const changeFunction = () => {
    // If you do `setMyFunction(secondFunction)`
    // It will be 'Guys and girls' that will be stored
    setMyFunction(() => secondFunction);
  };

  return (
    <button type="button" onClick={changeFunction}>
      Change the function stored: {myFunction.toString()}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Working with asynchronous code

In most case, React will batch your state updates to result in a single render. For example in useEffect / useLayoutEffect and in event handlers.

For example, when clicking on the button on the following code, will result in a single render with the new firstName and lastName:

function MyComponent() {
  const [firstName, setFirstName] = useState("Bob");
  const [lastName, setLastName] = useState("TheSponge");

  return (
    <button
      type="button"
      onClick={() => {
        setFirstName("Patrick");
        setLastName("Star");
      }}
    >
      Change name
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

But when you work with asynchronous code, for example if you fetch the new name with a REST API, it will result in multiple render:

function fakeAPI() {
  return new Promise((resolve) =>
    setTimeout(
      () =>
        resolve({ firstName: "Patrick", lastName: "Star" }),
      500
    )
  );
}

function MyComponent() {
  const [firstName, setFirstName] = useState("Bob");
  const [lastName, setLastName] = useState("TheSponge");

  return (
    <button
      type="button"
      onClick={async () => {
        const newName = await fakeAPI();

        // It will result into 2 render
        // firstName: 'Patrick' and lastName: 'TheSponge'
        // firstName: 'Patrick' and lastName: 'Star'
        setFirstName(newName.firstName);
        setLastName(newName.lastName);
      }}
    >
      Change name
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this case, we will prefer to do a single state which will have both firstName and lastName values because these values are tied together. But it can happen that updated values have no relationship but we sometimes need to update them together, in this case we will do separate state, and will have to make attention of the order of state updates.

Note: With React v18 these cases will not happen anymore, and updates will be also batched.

Note: In a next article, we will see how the batching of state works under the hood and how we can force the batching of updates in an application.


What not to do with states

This rule is valid for both Component class and Functional component. Do not mutate a state.
For example, don't do that:

function Person() {
  const [person, setPerson] = useState({
    firstName: "Bob",
    lastName: "TheSponge",
  });

  return (
    <div>
      <p>First name: {firstName}</p>
      <p>Last name: {lastName}</p>
      <button
        type="button"
        onClick={() =>
          setPerson(
            (prevState) => (prevState.firstName = "Romain")
          )
        }
      >
        Update firstName
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why won't it work?

When you call the update callback, React will compare with strict equality the previous state to the new one, if it's the same then React will not trigger a re-render.

Condition to trigger render from state updates

Note: And do person.firstName = 'Romain' will do nothing at all.


Conclusion

Using React state is not a hard thing and is really important to know how to work with it properly:

  • do not mutate the state
  • when you need the previous value of the state, prefer to use the version with callback

If you want to lazily initialize your state in Functional component, because of performance cost for example, think to use the callback initialization.

One last point, if the state is not used for the UI maybe the use of a state is not the right choice, a ref (useRef) would probably be a better option. It's something we will see in a next article :)


Do not hesitate to comment and if you want to see more, you can follow me on Twitter or go to my Website.

Top comments (3)

Collapse
 
jzombie profile image
jzombie

I clicked the "order" button a few times. very rapidly, in the async example and wound up with a negative value for "order in progress":

Order in progress: -1
Order delivered: 9

I think that the reasoning is because your setState calls aren't using the previous value directly in the call, and relying on the previous state which was set before the async handler started, resulting in stale state.

Collapse
 
romaintrotard profile image
Romain Trotard

Yep that's exactly the point of this example. If you rely on the previous value you need to use the callback API to get the real previous value ;)

Collapse
 
jzombie profile image
jzombie

Ah, I missed that part, thanks for the clarification!