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…
You probably want to add interactivity on your page and more particularly 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.
Before going into what you know nowadays i.e. hooks. It was a time when hooks did not exist and the only way to have a stateful component was to use Component class.
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>;
}
}
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>;
}
}
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>
);
}
}
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>
);
}
}
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>
);
}
}
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 surprise.
We have played enough with Component classes, now let’s see how to use a state in a Functional components.
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.
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.
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
.
const [counter, setCounter] = useState(0);
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.
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>
);
}
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>
);
}
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.
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.
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 :)
You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.