The complete guide to the useState hook in React.js

Introduction

Understanding state management is fundamental in React development. The arrival of Hooks in React 16.8 transformed how React developers manage state, with the useState hook being the most critical to learn and the most useful to adopt in your React components. In this blog post, we’ll look through React’s useState hook — the basics, including setting up your first hook and interacting with it, all the way to the advanced functionality.

Before the introduction of Hooks, which includes useState, managing state in React was mainly done through class components. When developers needed local state within a component, they would have to declare the component as a class.

In the class component, the state was initialized in the constructor method:

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

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

  render() {
    return (
      <button onClick={this.incrementCount}>
        Clicked {this.state.count} times
      </button>
    );
  }
}

React state was managed through this state property and the setState method. setState was used to update the component state and trigger a re-render.

While this worked well, it required the use of JavaScript classes, which could sometimes add unnecessary complexity. The introduction of Hooks and useState simplified state management by enabling it within function components, removing the need for classes and complex lifecycle methods.

Creating Your First useState Hook

The useState hook is a built-in function in React that allows you to add, use and manipulate local state inside your functional components. In a nutshell, a Hook is a unique function that lets you “hook into” React features. For example, useState is a Hook that allows you to add stateful logic into a function component.

Let’s dive in by creating a simple counter using useState.

Firstly, import the Hook at the top of your file:

import React, { useState } from 'react';

Next, initiate your useState hook within your function component. It takes one argument—the initial state—and returns an array of two values: the current state and a function to update that state. These values can be called whatever you’d like, but should be named something that helps indicate what the piece of data you’re working with is:

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

Here, count is the state variable, and setCount is the function to update this state. Whenever you press the button, setCount is triggered, increasing the counter state by one. Note that unlike the prior class component/setState code, there’s no additional functions and no constructors needed. The parameters returned from useState can be used anywhere inside of the current JavaScript scope.

Understanding Function Argument Syntax

As you begin using useState in more complex situations, you may find that your state becomes "out of date". One of the most common situations that developers will run into this is when you "batch" updates to setState:

const doUpdate() => {
  console.log(count) // 0
  setCount(count + 1)
  console.log(count) // 1

  // do other stuff  
  setCount(count + 1)
  console.log(count) // sometimes 2... sometimes 1! ack!
}

This can happen because React will attempt to "batch" multiple setState calls in order to improve your application’s performance.

The other situation that this behavior is undesired is when you are relying on the previous state value while calling the state update function. For instance, if you are appending values to an array:

console.log(fruits) // ["apple"]

setFruits([
   ...fruits,
   "banana"
])

// ... later

setFruits([
  ...fruits,
  "orange"
])

console.log(fruits) // ["apple", "orange"]

To fix this, you can use the alternative function syntax for useState:

const [fruits, setFruits] = useState([]);
setFruits(prevFruits => [
  ...prevFruits,
  "banana"
]} // ["apple", "banana"]

setFruits(prevFruits => [
  ...prevFruits,
  "orange"
]} // ["apple", "banana", "orange"]

The reason this fixes the previous issues with useState is the usage of the "previous state" function argument (prevFruits in the above example). The function inside setFruits gets the previous state as its argument, which ensures you are updating the state based on the correct previous state.

Complex Data Structures in useState

What if our state is an object or an array rather than a simple data type, like a number or string? No problem! useState perfectly handles complex data structures.

const [form, setForm] = useState({ username: '', password: '' });

const updateForm = e => {
  setForm({
    ...form,
    [e.target.name]: e.target.value,
  });
};

// In Form field
<input
  type="text"
  name="username"
  value={form.username}
  onChange={updateForm}
/>

Here’s an example of state as an object where we are maintaining a form with username and password fields. We use the spread operator (...) to clone the old state before updating it. In general, you should be using immutable functions to modify the data inside of a setState call. This is because React can have issues understanding that a change has occurred inside of a complex state value that it is tracking unless you make an immutable update to the value.

useState is super important for React developers to understand. While the basics can be grokked in just a few minutes, the complex interactions and alternative syntaxes are worth learning so that you aren’t left confused when it doesn’t work like you expected.