useState is one of the first hooks I came across while learning React. It is easy to understand/use but equally fascinating/confusing at the same time ( just on click of a button, things update on-screen 😮).
Let’s dive in!!!
Why useState Hook?
React does not detect change in normal variables that’s why useState hook is required.
For example, imagine you have a variable (let or var) in any react component that gets incremented on clicking any button. Also, it is being shown in the browser.
Now, on click of button, its value will be actually incremented but will not reflect in the browser.
Play with code here:- https://codesandbox.io/s/why-we-need-usestate-in-react-9uni8e
import React from "react";
const ComponentWithoutUseState = () => {
let val = 3;
return (
<>
<p>Current Val is {val}</p>
<p>
<button
onClick={() => {
val++;
console.log(val);
}}
>
increment val
</button>
</p>
</>
);
};
export default ComponentWithoutUseState;
Here variable “val” is being updated but for some reason, updated value is not reflected in UI.
Actually, component must be rendered again to reflect updated value. But regular variable change won’t trigger render. Furthermore, if variable is local variable, it would lose data after rendering component again (codesandbox example).
Check out official React doc for more https://react.dev/learn/state-a-components-memory#when-a-regular-variable-isnt-enough
And here comes useState for rescue.
What is useState Hook?
In React, useState hook is used to create and update state variables in functional components. It provides two functionalities:-
- Mechanism to access and update state variables.
- When there is state update in any component, rerender component and its children.
Rules of useState Hook
- useState hook must be used inside React functional component only.
- useState function can not be called inside conditions or loops (more on this later)
How To Use useState Hook?
- Like any other hook, first of all, import it:-
import React, { useState } from "react";
2. After that, inside functional component, we can call useState function with/without some initial value.
const [val, setVal] = useState(3);
useState returns an array of two elements (try doing console log of useState). First element is state variable which we can use as normal variables. Second element in array is setter function which can be used to update state variable.
Its general convention to use names of these two elements as variableName and setVariableName. However, it can be anything you wish.
const [x,y] = useState(3)
3. Now you’re ready to go!
const ComponentWithUseState = () => {
const [val, setVal] = useState(3);
const clickHandler = () => {
setVal(val + 1);
};
return (
<>
<div>ComponentWithUseState</div>
<p>Current Val is {val}</p>
<p>
<button onClick={clickHandler}>increment val</button>
</p>
</>
);
};
export default ComponentWithUseState;
Play with code here:- https://codesandbox.io/s/usestate-basics-isww8g
Here Comes The Confusion!!!
In above example, we are displaying variable “val” on browser screen and updating it on click of button using clickHandler function.
Inside clickHandler, we have setVal(val + 1) which will update “val” and rerender component so that UI reflects latest changes.
Everything is all good, right 😊?
Well if yes, try these two modifications of clickHandler function:-
const clickHandler = () => {
setVal(val + 1);
console.log(val);
};
const clickHandler = () => {
setVal(val + 1);
setVal(val + 1);
setVal(val + 1);
console.log(val);
};
You might notice these two issues:-
- In first case, browser is showing correct updated value but console.log is showing stale data.
- In second case, “val” is expected to increment by 3 but its being incremented by 1. Also, console.log is showing stale value.
Batching Is The One To Blame 🤨
Above behavior happens because of concept called “batching” in react. Read official docs on how react batches state updates.
Batching — In a single event handlerFunction (like onClick), all state variable updates are queued together for performance constraints in React.
This means, if we try to access “val” just after doing “setVal” in any event handler function, then we will receive stale value.
So doing setVal() 3 times is equivatlent to:-
setVal(3 + 1);
setVal(3 + 1);
setVal(3 + 1);
How To Avoid Batching?
There are two possible solutions:-
- Do all calculations beforehand and then update state variable only once in event handler function.
example.
const clickHandler = () => {
let tmp = val+1+1+1;
setVal(tmp);
console.log(tmp);
};
=================== Or =========================
// To update some textField data
const clickHandler = (e) => {
let tmp = e.target.value;
setVal(tmp);
console.log(tmp);
};
2. Use functional form of setter function. Official Docs on updating the same state multiple times before the next render
Functional Form Of Setter Function setVal()
setVal() also accepts function as an argument and provides current value of “val” as parameter of that function. And return value of that function will be updated value of state variable “val”.
For example, let initial value of val is 3.
const clickHandler = () => {
setVal((val) => {
console.log("current value is ", val); // output:- current value is 3
return val+1 // val will become 4
});
};
====================================================================
Same thing in short form
====================================================================
const clickHandler = () => {
setVal(val => val+ 1); // val will become 4
};
Let’s use it for our use case and see what’s different this time (sandbox for you to play):-
const clickHandler = () => {
setVal((val) => val + 1);
setVal((val) => val + 1);
setVal((val) => val + 1);
};
According to React docs, val => val+1
is called an updater function. When passed to setter function (setVal), it is queued to be processed after all code of that particular event handler is processed. Later during next render, React goes through this queue.
So react stores these three functions in the queue when we click on button:-
(val) => val + 1
(val) => val + 1
(val) => val + 1
====================================
On next render calculation done as follow:-
(3) => 3+1
(4) => 4+1
(5) => 5+1
React stores final value as 6
On next render, at line where we call useState (const [val,setVal] useState(3)
), React returns finally calculated value (ex. 6 in our case).
Are we missing something?
Remember console.log which was printing stale value…
const clickHandler = () => {
setVal((val) => val + 1);
setVal((val) => val + 1);
setVal((val) => val + 1);
console.log(val); // poor logger be like:- its 3, I'm sure 😢
};
Well, it is still doing same because it’s not added to queue. Maybe putting in in useEffect and running as side effect would be better idea.
useState Hook With Objects and Arrays
With arrays and objects, initialization of state variables is same as with JavaScript primitives.
const [userData, setUserData] = useState({
name: "",
email: "",
address: { city: "", country: "India" },
});
But while updating state, proper destructuring must be done. Great resource to checkout updating objects in state.
For example, if we want to update name, then doing this will delete all data and store name only:-
// This is wrong way to do it
const updateName = (e) => {
setUserData({ name: e.target.value });
};
It should be done like this:-
const updateName = (e) => {
setUserData({ ...userData, name: e.target.value });
};
Similarly, if we want to update city (nested object structure). We need to destructure nested objects as well.
const updateCity = (e) => {
setUserData({
...userData,
address: { ...userData.address, city: e.target.value },
});
};
// Notice how we need to destructure address as well
Play with code in updating objects with useState sandbox.
How Parent-Child Renders Work With useState?
If there is state variable change in parent, parent itself along with all its children are rendered again.
However, if child’s state changes then only child will render again without affecting parent.
If parent’s state is passed to child as prop and updated in child then parent along with all children will rerender.
Coding Our Own useState React Hook From Scratch
Sandbox link:- https://codesandbox.io/s/coding-our-own-usestate-26bv4t
// index.js
import React from "react";
import ReactDOM from "react-dom/client";
import MyReact from "./MyReact";
// Render funtion to rerender APP component after each setState
const render = () => {
root.render(<App />);
};
const { useState, resetIndex } = MyReact(render);
function App() {
const [val, setVal] = useState(3);
const [val2, setVal2] = useState(45);
console.log("App component rendered", val, val2);
resetIndex(); // At end of each component, it reset index
return (
<>
<p>
{val}{" "}
<button
onClick={() => {
// setVal(val + 1);
setVal((prev) => {
console.log(prev, "prevvalue");
return prev + 1;
});
}}
>
Update Val
</button>
</p>
<p>
{val2} <button onClick={() => setVal2(val2 + 1)}>Update Val2</button>
</p>
</>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
// MyReact.js
const MyReact = (render) => {
let state = []; // state array to keep record of all states created using useState
let index = 0;
const useState = (initialState) => {
const localIndex = index;
index++; // increment index
if (!state[localIndex]) state[localIndex] = initialState; // initialize state first time only
const stateUpdater = (newState) => {
state[localIndex] =
typeof newState === "function" ? newState(state[localIndex]) : newState;
render(); // re-render APP component after updating state
return newState;
};
return [state[localIndex], stateUpdater];
};
const resetIndex = () => (index = 0);
return { useState, resetIndex };
};
export default MyReact;
Reference:- https://youtu.be/1VVfMVQabx0?list=PLxRVWC-K96b0ktvhd16l3xA6gncuGP7gJ&t=54