If you are using ReactJS to build a website then you must have used useState to manage state. States are a great way to manage and render dynamic content in React. States can change over time, and React takes care of components to render with the state.
The simplest way to manage states in React is using hooks. The two hooks are useState and useReducer! Using these hooks we can manage the state inside any component easily. But these are local states, the state maintained inside one component cannot be used in some other components, and thatâs why it is named a local state.
On the other hand, we can also manage global state using props drilling or Context API or even using 3rd party state management libraries. But in this article letâs focus on how to manage the local state the right way!
Letâs see a basic usage of useState:
The count is managed locally and whenever the button is clicked the count is updated. This is pretty straightforward. But how about we extract the logic and make use of custom hooks?
Letâs create a useCount custom hook and manage the state in that custom hook. We will use the same example as above:
We made a few changes and our code is still working. With this, we now have a more clearer name â useCount and now the Component is independent of the implementation of useCount.
We initialize useState as follows:
Here we have two things: count and setCount. The count is a number and setCount is a function. Always remember that the first index in the array will always be an array and the 2nd index will always be a setter function. If we only want value and not function we can also write like this:
This way we will get access to count value alone. We can write anything inside the array, any name, any variable, everything is acceptable:
Whatever we write will not affect the working of useState but as a developer, there are some conventions that one should follow. If we write [count, increase] = useState(0), then other developers will never understand that increase is a setter function. Thus, a good practice is to always add a set as a prefix to a setter function. [count, setCount], [percentage, setPercentage], [isAdmin, setIsAdmin], ⌠these are good practices.
Even if you write like this, there will not be any errors but this is such a bad practice. In this setCount is a number and count is a setter function but no one will know this except you. Thus there are some conventions that we should always follow.
We know that setter functions are used to update the states. And whenever the state is updated, the component is re-rendered. Look at the following example:
Here we are simply updating the count value by 1 whenever the button is clicked. But what about the following following code?
If we click the button, it will trigger the Component to re-render with count=1. What would happen if we clicked the button again? It will invoke setCount(1) again, but as it is the same value it gets ignored and the Component wonât re-render. Letâs see another example to better understand this:
If we click the button 5 to 10 times, we will see that âRenderedâ is printed in the console only twice. This means the component has not re-rendered 5 to 10 times as React is getting the same state value on the button click.
But if we initialize the state with an object instead of a number, then the case will be different. Look at the below example:
We are setting the same object {value: 1} whenever the button is clicked so React shouldnât re-render the Component right? But no, if we click the button 10 times, the component will be rendered 10 times, because every time we click the button, a new object is created, and although the object is the same but their references are different so React will always assume that both the object is different and therefore state is updated and Component is re-rendered. Thatâs why the state should be simple like strings or numbers and only use objects or arrays whenever when there is no option.
Another way of updating the state is using functions.
Setter functions will always have access to previous values (this prevCount is also known as the latest count value). So we can use a function this way and can update the state. We donât need to have to write prevCount, we can write anything. Here is one more example for better understanding.
We have seen two ways to update the state, one is direct modification using values ad another is using function. So whatâs the difference between them. To understand this, first we need to understand that whenever we update a state, it is not updated immediately. Whenever we write useCount(count + 1) or useCount( (c) => c + 1), React will acknowledge and will schedule the update. And the value is updated in the next render and not immediately. Letâs see a simple example to understand this.
When the button is clicked, updateCount function is invoked. Here we are using setCount(count + 1) then a for loop and then we are printing the value of count. If we run this, we will see âLoop Endedâ is printed and count value is printed. Now we can observe, that even after the loop, the count value is still 0 and not 1. This is because React has acknowledged that the count value needs to be updated to 1 but it will only update it in the next render and not immediately. This is why React maintains two values viz, count and latestCount. Please note that the implementation of useState is quite complex and we are just simplifying it to better understand it.
React updates the states in batches thatâs why itâs important to understand how we are updating the states. See the following example:
When we clicked the button, we called setCount(count + 1) and said React to update the count by 1. So React will update the latestCount value from 0 to 1, but the count value will not updated immediately. Now, we call setCount(count + 1) 2nd time, and this time we tell React to update the count, so React will take the value of count (which is still 0 because the count has not been updated yet) and will add 1 and then assign to latestCount. Now latestCount is again 1, and the count is still 0. Then we call setCount(count + 1) the third time, and again this time, we are passing (count + 1) but as we know, React updated latestCount, not count, because count gets updated in batches, and the final value is updated in the next render. So even the third time, we pass (count + 1) where the count is still 0, and the latestCount is 1.
So even if we call setCount(count + 1) three times or 100 times, the count value will be increased by one only. This is because the setCount function uses (count + 1), where the count is passed as 0 every time.
But this is not the case if we update the state with functions. Because with the function, we have access to the latestCount and not count. Letâs see an example:
Here, we clicked the button and updated the state using setCount((c) => c + 1). React will acknowledge this and update the latestCount from 0 to 1, but the count value is still 0. Then, we again passed setCount((c) => c + 1) and told React to update the state. Now, the latestCount becomes 2, and the count remains 0. But how latestCount becomes 2? Itâs because we update the state using a function. (c)=> c + 1 This is passed in the setState, and here, c is not count, c is referred to as latestCount, so when the setState is called 2nd time with the latestCount, the latestCount value is increased. Then again, we called setState 3rd time, so the latestCount is updated to 3 from 2. Thus, the final count value becomes 3 in the next render.
That is why if you update the state using a value, i.e., calling setState(count + 1) 100 times, the final value will only be increased by 1 and not by 100 because every time, you are passing count in setState. But if you update the state using functions, i.e., calling setState((c) => c + 1) 100 times, then the final value is updated by 100 and not by 1 because here you are passing latestCount (c)and not the count value every time.
The React team, renowned for their expertise, has comprehensively explained this concept in their latest blog. This resource can be invaluable in deepening your understanding of updating state in React. You can learn more about this here .
We can initialize useState with a function that will be evaluated only in the first render. We can do something like this:
This is known as lazy initialization in React, where we pass a function to useState instead of an initial state value. This is particularly useful when the initial state requires heavy computation or when it depends on props that might change over time. It helps improve the appâs performance by delaying the computation until needed. The above example is ineffective because returning 0 doesnât require much computation.
Now that we have learned about useState, itâs time to dive deep into useReducer and its usage. useReducer is also another option for managing the React state. When your app grows and has a vast number of states, it could be more effective to use 10 or 20 useState; instead, we can use useReducer to manage the appâs huge state. Letâs see a simple usage of useReducer:
useReducer takes two parameters: reducer and initialState. We have defined our reducer function above, and our initial state is {count: 0} . The reducer function takes the state and the action. We can use if-else or switch statements and modify our state based on your action type. If the action type is INCREMENT, then we need to increase the count value by 1, and if the action type is DECREMENT, then we need to decrease the count value by 1.
Most people think that initialState should be an object but itâs not true. you can pass any primitive data type as well such as numbers or strings. Here is one example:
You can pass the init function as a third parameter in useReducer if you want to initialize the useReducer lazily. The parameters needed for the init function should be passed as a second parameter in useReducer. Here is an example:
We have learned useState and useReducer separately. But to understand these deeply, letâs ask: Can we implement useState using useReducer?
Itâs 100% possible to implement the useState using useReducer, and it is said that useState is implemented internally using useReducer. Please note that what we are implementing is a simple version, and React teams have implemented it more efficiently. Here is an implementatio code.
Now if we want to use it in our app, we can use it like this:
This is pretty straightforward, but letâs understand if you need to. We know that useState provides count and setCount, where setCount is a setter function. We can update the state by setCount(count + 1), where we are passing direct value in the useState. Otherwise, we can also pass the function to update the state, such as useState((prevCount) => prevCount + 1). So, in the reducer function, we will first identify whether we are passing a function or direct value. If we pass direct value, we will update the state, or else if we pass the function, then we will call that function with the currentState as a parameter.
We have seen how easily we can implement our custom useState using useReducer. Now letâs ask: Can we do vice-versa? Can we implement useReducer using useState?
Surprisingly, it is possible to implement useReducer with useState. Here is an example:
Now letâs see the usage:
Letâs understand the implementation. Our custom useReducer will also need a reducer function and an initialState. Whatever initialState is passed, we will set it and maintain the state using Reactâs useState. Our useReducer hook will return the state (which we are maintaining using Reactâs useState and dispatch function). We also created our dispatch function, which takes the currentState and action. Since the action is an object, we will pass that action on to the user-created reducer. As we know, a reducer takes state and action object, and based on the action type, it returns the modified state object, so we will store that new state in the variable newState. Then, we will pass the latest state to setState, which will update the state; hence, the overall state gets updated. This might seem unclear, but if you have hands-on useState and useReducer, you can understand the code easily.
We have come to an end, and we have learned the best practices related to useState and useReducer. We have also learned how to implement useState with useReducer and vice versa. I am pretty sure if the interviewer asks you questions on useState and useReducer, you will kill it!
If you've enjoyed reading this blog and have learnt at least one new thing, do subscribe to recieve updates whenever I post a new article directly to your inbox and do share on Twitter with your friends.