Some interesting points about React's useState hook
Som Shekhar Mukherjee
Posted on August 19, 2021
React's useState
hook is used to manage the state of your application and is seen quite often.
Today in this article I would like share some facts about this hook which you might not know and which might increase your understanding of this hook works.
๐ Setting state with a value similar to the current state will not cause a re-render.
Suppose you have a state foo
that's currently set to "Foo"
and you call setFoo
again with "Foo"
, it will not cause a re-render. Check the example below:
const App = () => {
const [foo, setFoo] = React.useState("Foo");
React.useEffect(() => {
console.log("Rendered");
});
return <button onClick={() => setFoo("Foo")}>Click</button>;
};
const rootEl = document.getElementById("root");
ReactDOM.render(<App />, rootEl);
๐ Updating state by passing in a callback
To set state we can either pass the new state value directly or we can pass in a function that takes as an argument the current state and returns the new state.
I prefer the second approach when my new state depends on the current state, for ex: setCount(currCount => currCount + 1)
instead of setCount(count + 1)
.
const Counter = () => {
const [count, setCount] = React.useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<>
<p>Current Count: {count}</p>
<button onClick={handleClick}>Add 3</button>
</>
);
};
const rootEl = document.getElementById("root");
ReactDOM.render(<Counter />, rootEl);
In example above although we call setCount
thrice but the count
only gets updated by one and this because React batches al these three calls.
So, suppose count
is currently 0
and you clicked the button. Now, what React sees is the following:
setCount(0 + 1)
setCount(0 + 1)
setCount(0 + 1)
React executes the first setCount
call after which count
becomes 1
and now the other two setCount
calls are essentially updating the state to what it already is and we saw in the previous section that React is smart enough to simply ignore this.
To fix this we need to update the count
state using the callback approach. So, we change the handleChange
function to the following:
const handleClick = () => {
setCount((currCount) => currCount + 1);
setCount((currCount) => currCount + 1);
setCount((currCount) => currCount + 1);
};
React will again batch all these calls, which you can confirm by adding the following in your code ("Rendered" should be logged only once).
React.useEffect(() => {
console.log("Rendered!");
});
So, when React encounters the first setCount
call the currCount
is 0
, so it is updated to 0 + 1
.
For the second setCount
call the currCount
becomes 1
, so it is updated to 1 + 1
and similar for the third call.
๐ Lazy Initializers
Suppose you've an input
and whatever your users type in the input gets stored in the localStorage
so that if the page reloads your users can continue from where they left.
The example below does exactly the same thing. So, to initialize the firstName
state we call the getDataFromLS
function which retrieves the data from localStorage
and whatever this function returns becomes the initial value of the firstName
state.
โ NOTE: Don't be confused that we have passed a function to useState
, we haven't. We've called it there itself, which means we've just passed the value that the function returns.
const getDataFromLS = (key) => {
console.log(`Retrieving ${key} from Local Storage`);
const value = window.localStorage.getItem(key) || "";
return value;
};
const App = () => {
const [firstName, setFirstName] = React.useState(
getDataFromLS("firstName")
);
return (
<>
{firstName && <h1>Hello {firstName}</h1>}
<form>
<div>
<label htmlFor="name">Your First Name: </label>
<input
id="name"
value={firstName}
onChange={({ target }) => {
localStorage.setItem("firstName", target.value);
setFirstName(target.value);
}}
/>
</div>
</form>
</>
);
};
const rootEl = document.getElementById("root");
ReactDOM.render(<App />, rootEl);
The initial value passed to useState
is only used for the first time. On subsequent calls to useState
React figures out that this is not the first time that this hook is being called and so it doesn't set the state variable to its initial value but instead sets it to its current value.
But, if you open the devtools and see the logs you would see that
for every re-render the getDataFromLS
function is called (confirmed by the logs).
So, even if React doesn't care what getDataFromLS
function returns on subsequent calls it still calls that function.
This is quite okay for this example but this could impact performance if you're doing some complex operations to get that initial value.
Let's add another input
field but this time we will set the initial value differently.
const getDataFromLS = (key) => {
console.log(`Retrieving ${key} from Local Storage`);
const value = window.localStorage.getItem(key) || "";
return value;
};
const App = () => {
const [firstName, setFirstName] = React.useState(
getDataFromLS("firstName")
);
const [lastName, setLastName] = React.useState(() =>
getDataFromLS("lastName")
);
const handleChange = () => {};
return (
<>
{(firstName || lastName) && (
<h1>
Hello {firstName} {lastName}
</h1>
)}
<form>
<div>
<label htmlFor="name">Your First Name: </label>
<input
id="name"
value={firstName}
onChange={({ target }) => {
localStorage.setItem("firstName", target.value);
setFirstName(target.value);
}}
/>
</div>
<div>
<label htmlFor="name">Your Last Name: </label>
<input
id="name"
value={lastName}
onChange={({ target }) => {
localStorage.setItem("lastName", target.value);
setLastName(target.value);
}}
/>
</div>
</form>
</>
);
};
const rootEl = document.getElementById("root");
ReactDOM.render(<App />, rootEl);
So, this time instead of calling the function there itself, we passed a function to useState
which React will call (not us) and whatever this function returns is set as the initial state. This is referred to as "Lazy Initialization".
โ NOTE: React calls this function synchronously so this function cannot be asynchronous.
Now, you would only see the "Retrieving lastName from Local Storage" log once (unless the component gets unmounted and mounted again) but you would see the "Retrieving firstName from Local Storage" every time the component re-renders.
That's It! ๐ค
Hope, you found this useful and learned something new. Let me know your thoughts in the comments.
Posted on August 19, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
November 27, 2024