React is a free, open-source JavaScript frontend library that we can use to build frontend applications. Before React v16.8 in 2019, developers always used class components for data management (with states) and other operations like lifecycle methods, and functional components were only to be used for rendering UI.
Since the introduction of React Hooks in React v16.8, we can manage data via states in functional components and work with lifecycle methods. Over time, class components were outdated and deprecated in React. Functional components along with Hooks are the new standard way to write React components.
#State in React
Whenever an interaction happens, react components often need to change and show the latest data after an interaction. For instance, typing in a form should update the input field with whatever the user typed, clicking “next page” should change the page content, and clicking “Add contact” should add the new contact to the existing contact list.
To update the UI, components first need to “remember” these things: the current input value, the current page, and the current contact list. This kind of component-specific memory is called state.
For example:
import { Box, Button, Heading } from "@chakra-ui/react";export default function CounterExample() {let count = 0;function handleClick() {count = count + 1;}return (<><Box p={8}><Button onClick={handleClick}> Increment </Button><Heading>{count}</Heading></Box></>);}
In this code above, we have defined a local variable count
and an increment button, which tries to increment the count value by one every time it is clicked. However, if we try to run this code and click the increment button it doesn’t work as expected. This happens due to two reasons
- The value of a local variable does not persist across renders, meaning that on every re-render count will be re-initialized to 0.
- Local variables do not trigger re-renders, so when we click the Increment button, React would not detect a state change and would not re-render our component.
We need to have “state” here instead of the local variable to manage the memory of a component.
#useState Hook
The useState() hook:
- Allows us to obtain a state variable, this is a special variable that is capable of retaining data between renders.
- Provides us with a setter function to update the state variable and hence trigger a re-render of our component.
We can import the useState hook from react. The useState() hook takes in the initial value of the state variable as an argument and provides us with the state variable and the setter function. The variable could be of any data type, such as string, number, object, array, and more.
For Example:
import { useState } from "react";const App = () => {const [number, setNumber] = useState(1);const [string, setString] = useState('John Doe')const [object, setObject] = useState({name: 'johndoe'})const [array, setArray] = useState([1,2,3])return (// ...);};
This setter function can be called anything, but it is a general practice to use the variable name with a prefix of set
. For example - name, setName | count, setCount | and so on.
const [name, setName] = useState('John Doe')const [count, setCount] = useState(0)const [anything, setAnything] = useState({})
To fix our CounterExample component where we could not see the updates in the UI we can use the useState hook. Here’s how we can use it to fix the situation:
import { Box, Button, Heading } from "@chakra-ui/react";import { useState } from "react";export default function CounterExample() {const [count, setCount] = useState(0);function handleClick() {setCount(count + 1);}return (<><Box p={8}><Button onClick={handleClick}>Increment </Button><Heading>{count}</Heading></Box></>);}
#Updater functions
Going a step further, we should understand that the setter function will always have access to the value of the state variable in the current render. Let us take the example of the counter component we have built above. Try adding some logs before and after calling the setCount and hit the increment button:
// ...const [count, setCount] = useState(0);function handleClick() {console.log("Before Set Count", count);setCount(count + 1);console.log("After Set Count", count);}// ...
You might have expected it to print 1 in the After Set Count, but it logged a 0 there as well.
// ...const [count, setCount] = useState(0);function handleClick() {console.log("Before Set Count", count); // Prints - Before Set Count 0setCount(count + 1);console.log("After Set Count", count); // Prints - After Set Count 0}// ...
This happens because in the entire execution context of the handleClick function, the value of count was initially 0 and the operations by setCount will take effect in the next render.
Next, let us take a look at this function below:
// ...const [count, setCount] = useState(0);function handleClick() {console.log("Before Set Count", count);setCount(count + 1);setCount(count + 1);setCount(count + 1);console.log("After Set Count", count);}// ...
Okay, two questions arise here:
- What do you think will be printed in the logs?
- What will be the value of the count variable in the next render?
For 1, it will print 0 as the function will have access to the count value of the current render.
// ...const [count, setCount] = useState(0);function handleClick() {console.log("Before Set Count", count); // Prints - Before Set Count 0setCount(count + 1);setCount(count + 1);setCount(count + 1);console.log("After Set Count", count); // Prints - After Set Count 0}// ...
For 2, the value of count in the next render will be 1 and not 3, even though we called setCount(count+1)
thrice.
This is what actually happens, we are just calling setCount(0+1)
thrice.
// ...const [count, setCount] = useState(0);function handleClick() {console.log("Before Set Count", count);setCount(count + 1); // setCount(0+1) = setCount(1)setCount(count + 1); // setCount(0+1) = setCount(1)setCount(count + 1); // setCount(0+1) = setCount(1)console.log("After Set Count", count);}// ...
You may run into a situation where you want to access the latest value of a state variable and update it in that case you can use updater functions as shown below:
// ...function handleClick() {console.log("Before Set Count", count);setCount(count => count + 1);setCount(count => count + 1);setCount(count => count + 1);console.log("After Set Count", count);}// ...
As you can see, instead of passing a value to setCount we passed a function, this function gets the latest value of the variable as a parameter and returns an incremented value and this is how it will behave:
// ...const [count, setCount] = useState(0);function handleClick() {console.log("Before Set Count", count); // Prints - Before Set Count 0setCount(count => count + 1); // setCount(0 => 0+1) = setCount(1)setCount(count => count + 1); // setCount(1 => 1+1) = setCount(2)setCount(count => count + 1); // setCount(2 => 2+1) = setCount(3)console.log("After Set Count", count); // Prints - After Set Count 0}// ...
#Using array/object as State
At times we need to store many things for a particular entity, for example - we can choose to have name, age, and hobby as state variables for a person.
const App = () => {const [name, setName] = useState("John Doe");const [age, setAge] = useState(20);const [hobby, setHobby] = useState("Reading");return (// ...);};
Instead of creating three different state variables, it would be better to create an object that stores the state of a person. We can combine name, age, and hobby properties into an object state and use it as shown in the template.
const App = () => {const [userDetails, setUserDetails] = useState({name: "John Doe",age: 20,hobby: "Reading",});return (<div><h1>{userDetails.name}</h1><p>{userDetails.age} || {userDetails.hobby}</p></div>);};
So far we have seen numbers, and strings as state variables, these JavaScript values are “immutable”, so if we replace them we can trigger a re-render.
For example:
const [distance, setDistance] = useState(5);// ...setDistance(15)
When we set the distance from 5
to 15
, the value 5
itself doesn’t change. 5
is still 5
But when we do something as shown below, we are mutating
the state, and the original userDetails
object itself changes. This is known as a mutation
. This is not allowed and would not lead React to re-render our component.
userDetails.name = 'Jane Doe'
It is important to remember that we cannot mutate a state variable of type objects/arrays. We always need to replace them entirely or use the spread operator.
// This is IncorrectuserDetails.name = 'Jane Doe'// This is CorrectsetState({...userDetails,name: 'Jane Doe'})// This is CorrectsetState({name: 'Jane Doe',age: 20,hobby: "Reading",})
Similarly, we can use arrays to store some data as shown
const [todoList, setTodoList] = useState(["Buy Milk","Buy Bread","Fix Bugs"]);
When updating the array state, we must avoid methods like push
, pop
, shift
, unshift
, splice
, reverse
, and sort
as these methods mutate the original array. Instead, we should use options like concat
, spread
syntax ([...arr])
, filter
, slice
, and map
as these methods return an entirely new array.
// This is IncorrecttodoList.push('Exercise')// This is CorrectsetState([...todoList,'Exercise'])
#Caveats
useState
is a hook, so just like any other hook, we should only use the useState() hook at the top level of our component: We should not use it inside any function, loop, nested function, or conditions. This helps React preserve and call hooks in the same order each time a component renders.
// Do not do thisif (condition) {const [count, setCount] = useState()(0);}// Do not do thisfor (let index = 0; index < 25; index++) {let [count, setCount] = useState()(0);}// Do not do thisconst nestedFn = () => () => {const [count, setCount] = useState()(0);};
#Conclusion
In this guide, we have learned what state is and why it is important, we learned about the useState() hook from React which helps us to manage a component’s memory. We also learned about the lifecycle of a state variable and how the new state value is enforced after a component’s re-render. Finally, we checked how to use objects and arrays as state variables and wrapped up by going through a few caveats about using hooks.