In React, state handling can be confusing for beginners.
This article aims to help you understand when and when not to use it.
useState
When
One way to think about when to use useState is related to the UI:
If your UI is updated, it should be from a useState, either from a useState in your component or in a parent or context.
Keep in mind that there can be use case and exceptions to this rule, but for 90% of your work, this works.
When not to
The opposite is also to be stated:
If your UI is not updated from this state, you should NOT be using useState
Use cases
API Calls
You should use a library that handles it for you, like react-query
.
However if you do not want to use it, you need to think, will you display your API results? If yes then use a useState.
Do not forget to also display a loading state, this is a must have when using APIs.
const ShowTable = () => {
// At first we are displaying a loading and have no data
const [{ isLoading, data }, setAPICallState] = useState({
isLoading: true,
data: [],
});
// perform the API call on mount
useEffect(async () => {
const result = await (await fetch("myapi.url")).json();
// now set the state on success. Do not forget to handle errors if you copy paste this code
setAPICallState({ isLoading: false, data: result });
});
if (isLoading) {
return "Is loading";
}
return <Table data={data} />;
};
Hidden actions
Are you tracking user actions?
You should not need an action if this tracking is hidden/silent.
Are you doing computations?
If the results of the computation are displayed, yes, if not, you should not.
State updates depending on other state updates
This is now a bit trickier.
Usually if states starts to be too co-dependent it's a warning sign to approach you problem from another angle.
The example bellow will highlight this.
Each UI state should match one state update.
If you update X times states separately, you should have X different UI state displayed.
Checkout the 4th step of the example bellow to see it live
Example:
State updates depending on other state updates
In this example, we are expecting to:
- 1 - Get all cities
- 2 - Get the details of the first city in the list
- 3 - Display the results
const cityCountry = {Paris: "France",Lisbon: "Portugal",};// fake api callsconst getCities = () =>new Promise((resolve) => {setTimeout(() => resolve(["Paris", "Lisbon"]), 1000);});const getCity = (city) =>new Promise((resolve) => {setTimeout(() => resolve({ city, country: cityCountry[city] }), 1000);});// This component will get all cities, then display more info from the first oneconst ShowCitiesAndCityState = () => {const [cities, setCities] = React.useState([]);const [cityDetails, setCityDetails] = React.useState({});React.useEffect(() => {getCities().then((data) => {setCities(data);getCity(cities[0]).then((cityDetails) => setCityDetails(cityDetails));});}, []);return (<div><div>Cities: {cities}</div><div>Country of first city: {cityDetails.country}</div></div>);};render(<ShowCitiesAndCityState />);
Ouch, it doesn't work.
We should be seeing:
Cities: ParisLisbon
Country of first city: France
BUT you see:
Cities: ParisLisbon
Country of first city:
Can you spot what is the are the issues?
1 - Add a loading
Always show a loading when making API calls!
const cityCountry = {Paris: "France",Lisbon: "Portugal",};// fake api callsconst getCities = () =>new Promise((resolve) => {setTimeout(() => resolve(["Paris", "Lisbon"]), 1000);});const getCity = (city) =>new Promise((resolve) => {setTimeout(() => resolve({ city, country: cityCountry[city] }), 1000);});// This component will get all cities, then display more info from the first oneconst ShowCitiesAndCityState = () => {const [isLoading, setIsLoading] = React.useState(true);const [cities, setCities] = React.useState([]);const [cityDetails, setCityDetails] = React.useState({});React.useEffect(() => {getCities().then((data) => {setCities(data);getCity(cities[0]).then((cityDetails) => {setCityDetails(cityDetails);setIsLoading(false);});});}, []);if (isLoading) {return "Is loading";}return (<div><div>Cities: {cities}</div><div>Country of first city: {cityDetails.country}</div></div>);};render(<ShowCitiesAndCityState />);
3 - Avoid depending on state result for computation
Simply forward the data directly if you can:
const cityCountry = {Paris: "France",Lisbon: "Portugal",};// fake api callsconst getCities = () =>new Promise((resolve) => {setTimeout(() => resolve(["Paris", "Lisbon"]), 1000);});const getCity = (city) =>new Promise((resolve) => {setTimeout(() => resolve({ city, country: cityCountry[city] }), 1000);});// This component will get all cities, then display more info from the first oneconst ShowCitiesAndCityState = () => {const [isLoading, setIsLoading] = React.useState(true);const [cities, setCities] = React.useState([]);const [cityDetails, setCityDetails] = React.useState({});React.useEffect(() => {getCities().then((data) => {setCities(data);getCity(data[0]).then((cityDetails) => {// consecutive state updates are bundled togethersetCityDetails(cityDetails);setIsLoading(false);});});}, []);if (isLoading) {return "Is loading";}return (<div><div>Cities: {cities}</div><div>Country of first city: {cityDetails.country}</div></div>);};render(<ShowCitiesAndCityState />);
Yay it works!
The issue is that state update are asynchronous.
When we were executing getCity(cities[0])
, cities[0]
was not updated yet since state updates are not synchronous.
The solution is to avoid depending on state but instead on the data directly.
4 - Understanding uncessary state update calls
Now this is extra just for you.
In this example, we perform 3 renders
-
1 - component mount
isloading: true
cities: []
cityDetails: {}
-
2 - First re render, because
setCities
was called:isloading: true
cities: ['Paris', 'Lisbon']
cityDetails: {}
-
3 - Second re render, because
setCityDetails
was called:isloading: true
cities: ['Paris', 'Lisbon']
cityDetails: {city: 'Paris', country: 'France'}
We have 3 renders, while on the UI we only show
- 1 Is loading
- 2
Cities: ParisLisbon Country of first city: France
we have 2 UI states yet 3 re renders. Let's update the code.
- Either you set state consecutively, React will update the states at the same time.
- Or you use a single state for both
cities
andcityDetails
const cityCountry = {Paris: "France",Lisbon: "Portugal",};// fake api callsconst getCities = () =>new Promise((resolve) => {setTimeout(() => resolve(["Paris", "Lisbon"]), 1000);});const getCity = (city) =>new Promise((resolve) => {setTimeout(() => resolve({ city, country: cityCountry[city] }), 1000);});// This component will get all cities, then display more info from the first oneconst ShowCitiesAndCityState = () => {const [isLoading, setIsLoading] = React.useState(true);const [cities, setCities] = React.useState([]);const [cityDetails, setCityDetails] = React.useState({});React.useEffect(() => {getCities().then((data) => {getCity(data[0]).then((cityDetails) => {setCities(data);setCityDetails(cityDetails);setIsLoading(false);});});}, []);if (isLoading) {return "Is loading";}return (<div><div>Cities: {cities}</div><div>Country of first city: {cityDetails.country}</div></div>);};render(<ShowCitiesAndCityState />);