React Hooks Explained: useState( )

Nowadays, managing state is the most crucial part in any application's architecture. Most applications behaviour depends on the values of states defined in them, So understanding how to manage it efficiently becomes very important. Before hooks introduction in React version 16.8, the only way to use state in your application is through class component. But now with the help of useState hook we can manage state in our functional components too. So, in this article we will be learning everything that we need to know about useState to get started with stateful functional components.

Comparing State Management in classes and functions

Lets start by understanding the use of useState hook by looking at an example of a simple counter application written using React's functional component.

import React, { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  const [msg, setMsg] = useState('Use the below button to increase the count');

  return (
    <div>
      <p>Counter: {count}</p>
      <p>{msg}</p>
      <button onClick={() => setCount(count + 1)}>Count</button>
    </div>
  );
}

For comparison, lets also rewrite it into a class component.

import React, { Component } from 'react';
export class CounterClass extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      msg: 'Use the below button to increase the count',
    };
  }

  render() {
    return (
      <div>
        <p>CounterClass: {this.state.count}</p>
        <p>{this.state.msg}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Count
        </button>
      </div>
    );
  }
}

Okay now lets compare each aspect one by one.

Defining initial state

In a class component, initial state is defined as an object inside the constructor containing all the state for the component.

constructor(props) {
  super(props);
  this.state = {
    count: 0,
    msg: 'Use the below button to increase the count',
  };
}

But in a functional component, we define the initial state by passing it as an argument in the useState hook.

useState(initialState);

The return value of useState hook is an array containing the current state and a function to update the value of current state.

const [state, setState] = useState(initialState);

Now, like in a class component we can define all state for a component in a single useState hook.

const [state, setState] = useState({
  count: 0,
  msg: 'Use the below button to increase the count',
});

But it is a recommended practice to use individual useState hook for managing each state. As it is cleaner and easier to maintain.

const [count, setCount] = useState(0);
const [msg, setMsg] = useState('Use the below button to increase the count');

Now, there can be situations where the initial state you are defining may require time to get resolve. Passing this as initial state in useState hook can slow down the whole application. As you know, in functional components the initial state is declared in the render function and its value updates at every render. This is not a problem in class component as the initial state is defined in the constructor which is called only once at the start.

But there is a solution, useState also take function as the argument. the useState will run this function only once when the component is rendered first time. We can pass the function in useState like this

useState(() => {
  // Some heavy computation task
});

Updating the State

In class component, we can update the count by calling this.setState.

this.setState({ count: this.state.count + 1 });

Or by returning the updated value of count from a function in this.setState.

this.setState((prevState) => {
  return { count: prevState.count + 1 };
});

In functional components, as we are using individual useState for each state. We can easily update the value of count by calling the setCount function like this

setCount(count + 1);

But if you are depending on the previous state for updating to new state. It is recommended to use the function in setState like this

setCount((prevCount) => prevCount + 1);

The reason behind this is say you want to update the state two times in a function and you try to do it like this

export function Counter() {
  const [count, setCount] = useState(0);
  const [msg, setMsg] = useState('Use the below button to increase the count');

  return (
    <div>
      <p>Counter: {count}</p>
      <p>{msg}</p>
      <button
        onClick={() => {
          setCount(count + 1);
          setCount(count + 1);
        }}
      >
        Count
      </button>
    </div>
  );
}

But you will see that the count value is still updating by one. This is because the count value in the setCount is the same when we render our functional component and count value doesn't change inside of the function from where it is called. So, in the above code the count value is same in both setCount, overriding eachothers value resulting in value of count increased by just one.

Now, if we use the function in setCount. We can get the desired result as the updated count value gets stored in the prevCount and we can use the prevcount to correctly update the value of count inside the function.

export function Counter() {
  const [count, setCount] = useState(0);
  const [msg, setMsg] = useState('Use the below button to increase the count');

  return (
    <div>
      <p>Counter: {count}</p>
      <p>{msg}</p>
      <button
        onClick={() => {
          setCount((prevCount) => prevCount + 1);
          setCount((prevCount) => prevCount + 1);
        }}
      >
        Count
      </button>
    </div>
  );
}

Lastly, if you are using the single useState hook to manage all states like this

const [state, setState] = useState({
  count: 0,
  msg: 'Use the below button to increase the count',
});

You need to remember that when updating only the value of count. Unlike this.setState, setState will overwrite the whole state object to the new object having only value of count. You can see in the output of the code below that after clicking the count button the message will get dissappear.

export function Counter() {
  const [state, setState] = useState({
    count: 0,
    msg: 'Use the below button to increase the count',
  });

  return (
    <div>
      <p>Counter: {state.count}</p>
      <p>{state.msg}</p>
      <button onClick={() => setState({ count: 1 })}>Count</button>
    </div>
  );
}

In order to avoid this you will need to pass the old state with the new state in setState.

export function Counter() {
  const [state, setState] = useState({
    count: 0,
    msg: 'Use the below button to increase the count',
  });

  return (
    <div>
      <p>Counter: {state.count}</p>
      <p>{state.msg}</p>
      <button
        onClick={() =>
          setState((prevState) => {
            // Expanding prevState object using spread operator
            return { ...prevState, count: 1 };
          })
        }
      >
        Count
      </button>
    </div>
  );
}

Conclusion

useState provides a cleaner and a maintainable way to manage states in an application. After reading this article you are ready to start using useState in your react projects like a pro.

19