Some interesting points about React's useState hook

somshekhar

Som Shekhar Mukherjee

Posted on August 19, 2021

Some interesting points about React's useState hook

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);
Enter fullscreen mode Exit fullscreen mode

๐Ÿš€ 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);
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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!");
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
somshekhar
Som Shekhar Mukherjee

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

ยฉ TheLazy.dev

About