React Hooks: Managing State With useState Hook

Featured on Hashnode

Subscribe to my newsletter and never miss my upcoming articles

Hello World 👋

Hooks are special types of functions in React that you can call inside React functional components. They let you store data, add interactivity, and perform some actions, otherwise known as side-effects.

Below are the most common hooks that you use:

  • useState
  • useEffect
  • useRef
  • useContext
  • useReducer

In this article, we will learn in-depth about useState hook.

useState

useState is a built-in function in React. It takes a single argument and returns an array of two elements when executed.

Let's see an example.

const [count, setCount] = useState(0)
  • It is used to manage the state of the component.
  • When the state of a component changes, React re-renders that component and all of its child components automatically.
  • It takes the initial value of the state as an argument.
  • It returns an array of two elements.
    • The first element is the value of the state.
    • The second element is a function that you can use to set the value of the state
    • You can name these elements whatever you like, but the common practice is to name them as var and setVar. For instance, in the above example, we named it as count and setCount.

In the above example, we called useState with the argument of 0. This means that the initial value of the state is 0. count contains the value of the state. setCount is a function you can use to set the value of count.

Let's see the complete example of a component to get an idea of how useState is used in React.

import React, { useState } from "react"

function Counter() {
    const [count, setCount] = useState(0)
    function increment() {
        setCount(count + 1)
    }
    return (
        <button onClick={increment}>{count}</button>
    )
}

This renders a simple button that shows the value of count. Initially, it is 0. Whenever you click on the button, the count value is increased by 1 by using setCount. And as soon as the state changes, the component rerenders and the new value of count is shown in the button.

Functional Updates

Let's slightly change the above component.

import React, { useState } from "react"

function Counter() {
    const [count, setCount] = useState(0)
    function incrementBy3() {
        setCount(count + 2)
        setCount(count + 1)
    }
    return (
        <button onClick={incrementBy3}>{count}</button>
    )
}

Now, when you click on the button, what would you expect the increment to be. Will the count be incremented by 2? (or) Will it be incremented by 1? (or) Will it be incremented by 3?

Click and try it out.

Here is the corresponding code sandbox for it.

You can observe that the count will only be incremented by 1. This is because of how React makes state updates. Many state updates are batched together and performed in an asynchronous manner. So, if you have two setState calls at the same place, you cannot rely on React to complete the first state update before doing the second state update.

Let's take the above example.

  • Initially, the count is 0.
  • Button is clicked.
  • First, React starts executing setCount(count + 2).
    • The value of count in that render is 0.
    • React calculates the value of count for the next render to be count + 2, which is 2.
    • But the component is not yet re-rendered. So the current value of the variable count is still 0.
  • Now, React starts executing setCount(count + 1).
    • Since the value of count is still 0, React calculates the value of count for the next render to be count + 1 which is 1.
    • The value of next state was 2 when setCount(count+2) is executed. Now, it got overridden by the value of next state of setCount(count+1) which is 1.
  • Now since all the state updates are executed, React starts rerendering the component with the value of the next state which is 1.
  • And this is the reason why, when you click on the button only 1 is getting incremented instead of 3.

Even though it takes a while to understand the problem that is occurring, the solution to fix this problem is not that complicated.

Till now we have seen that setCount takes a value as an argument. But it also takes a callback as the argument of setCount. The first argument of that callback function is the previous value of the state.

For example, if we want to increment the count by 1, you can do it as follows:

setCount(previousCount => previousCount + 1)

When you use this type of callback function to update the state, you can be assured that the prevCount will always have the correct value of the state even though the component is not yet rerendered. That is why it is always recommended to use this type of update whenever the next value of the state is computed from the previous value of the state.

Let's use this approach and rewrite the above example.

import React, { useState } from "react"

function Counter() {
    const [count, setCount] = useState(0)
    function incrementBy3() {
        setCount(previousCount => previousCount + 2)
        setCount(previousCount => previousCount + 1)
    }
    return (
        <button onClick={incrementBy3}>{count}</button>
    )
}

This correctly increments the count by 3.

Lazy Initialization

const initialValue = resultOfSomeExpensiveOperation()
const [state, setState] = useState(initialValue)

Previously we have seen that the useState takes the initial value as an argument.

Technically React only needs the initialValue when the component is first mounted. After that, the initialValue is no longer applicable. So if the initial value is calculated through some expensive operations, we want those operations to run only at the start. Let's see if that is actually happening or not through an example.

import React, { useState } from "react";

function getInitialValue() {
  console.log('getInitialValue is getting executed');
  // ... do some expensive operations
  return 0;
}

function Counter() {
  const [count, setCount] = useState(getInitialValue());
  function increment() {
    setCount(count + 1);
  }
  return <button onClick={increment}>{count}</button>;
}

Try to click on the button and check the console.

  • You will see that the getInitialValue function is being executed every time the button is clicked. So, it is getting called on every rerender.
  • So, if there are any expensive operations to be made in that function, they will be executed after every rerender.
  • But only the first execution of the getInitialValue is useful for React. All others will be thrown away as the state is already set in the subsequent rerenders.
  • You can see why this might cause performance problems for your application.

React gives us a way to handle this type of situation. It's called Lazy Initialization of State.

Instead of directly passing the value as an argument, you have the option to pass a callback function which when executed gives you the initial value. React executes this function only when it is needed. This is needed only at the first, so React executes this function only once at the start.

Let's rewrite the above example to use lazy state initialization.

import React, { useState } from "react";

function getInitialValue() {
  console.log('getInitialValue is getting executed');
  // ... do some expensive operations
  return 0;
}

function Counter() {
  const [count, setCount] = useState(() => getInitialValue());
  function increment() {
    setCount(count + 1);
  }
  return <button onClick={increment}>{count}</button>;
}

All we changed in the above example is that: useState(getInitialValue()) is changed to useState(() => getInitialValue()).

You can check the console of the above code sandbox. You will see that getInitialValue is not getting called when you click on the button. It is only called at the start.

useState with Objects

You can manage any type of state with useState hook, even objects.

For example, let's use useState hook to manage firstName and lastName states in a single object.

const [name, setName] = useState({
    firstName: 'Bhanu Teja',
    lastName: 'P'
})

Now, whenever you call setName to update the name object, you have to provide both firstName and lastName.

For example,

setName({
    firstName: 'New First Name',
    lastName: 'New Last Name'
})

What if we want to update only the firstName or only the lastName. You can do so by using the spread operator.

function setFirstName(firstName) {
    setName({
        ...name,
        firstName
    })
}

function setLastName(lastName) {
    setName({
        ...name,
        lastName
    })
}

Let's put everything together.

import React, { useState } from "react";

function App() {
  const [name, setName] = useState({
    firstName: "Bhanu Teja",
    lastName: "P"
  });

  function setFirstName(firstName) {
    setName({
      ...name,
      firstName
    });
  }

  function setLastName(lastName) {
    setName({
      ...name,
      lastName
    });
  }

  return (
    <>
      <input
        placeholder="First Name"
        type="text"
        value={name.firstName}
        onChange={(e) => setFirstName(e.target.value)}
      />
      <input
        placeholder="Last Name"
        type="text"
        value={name.lastName}
        onChange={(e) => setLastName(e.target.value)}
      />
      <p>
        Your name is:{" "}
        <strong>
          {name.firstName}.{name.lastName}
        </strong>
      </p>
    </>
  );
}

Usage of useState with arrays is very similar to that of with objects.

What have you learned?

You learned about:

  • What are hooks and what are some of the common hooks in React ?
  • useState hook:
    • It takes the initial value of the state as an argument and returns an array of two elements - one has the value of the state and the other is a function to update the value of the state.
    • How to do functional updates for the state?
    • Why, not using functional updates, causes a problem in certain situations ?
    • It is always recommended to use functional updates to update the state when the next state is computed from the previous state.
    • Lazy initialization of state and when it can be useful.
    • Usage of useState with objects and arrays.

What's Next?

In the next article, we will learn everything about useEffect hook.

Until Next Time 👋


If this was helpful to you, Please Like and Share so that it reaches others as well. To get email notifications on my latest articles, please subscribe to my blog by hitting the Subscribe button at the top of the page. You can also follow me on Twitter @pbteja1998.

No Comments Yet