Things you need to know about React state
Romain Trotard
Posted on February 23, 2022
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
ofReact
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>;
}
}
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>;
}
}
- 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>;
}
}
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>
);
}
}
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>
);
}
}
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>
);
}
}
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>
);
}
}
- 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>
);
}
}
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>;
}
- 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>;
}
What is the difference between the following initialization for you?
useState(initializeState());
And
useState(initializeState);
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');
So then I just have to use the value
.
Note: You can name the
value
and theupdater
with the name you want, because there is a destructuring of the array :)
const [counter, setCounter] = useState(0);
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>
);
}
- 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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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.
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.
Posted on February 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.