Using useState with React

reactbasics

Understanding how to use useState in React


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
Editable React Code
const cityCountry = {
Paris: "France",
Lisbon: "Portugal",
};
// fake api calls
const 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 one
const 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 />);
Result

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!

Editable React Code
const cityCountry = {
Paris: "France",
Lisbon: "Portugal",
};
// fake api calls
const 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 one
const 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 />);
Result

3 - Avoid depending on state result for computation

Simply forward the data directly if you can:

Editable React Code
const cityCountry = {
Paris: "France",
Lisbon: "Portugal",
};
// fake api calls
const 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 one
const 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 together
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 />);
Result

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 and cityDetails
Editable React Code
const cityCountry = {
Paris: "France",
Lisbon: "Portugal",
};
// fake api calls
const 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 one
const 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 />);
Result