The React useState hook

Published January 3, 2022

The React useState hook is used to add local state to a component. When useState is used, React will preserve the component's local state between re-renders of that component.

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

The useState hook returns a pair: a stateful value, and a function to update it. During the initial render, the returned state referenced by the variable state is the same as the value passed as the first argument, which is initialState. The one argument that useState supports is used to provide an initial value.

The setState function above is used to update the state. It accepts a new state value and enqueues a re-render of the component. More on this later.

A useState counter example

const StateCounter = () => {
  const [count, setCount] = useState(0);

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

In this example, the count variable is incremented by one every time the button is clicked. useState(0) initializes count to zero when the component first renders. count holds the current number of clicks, while setCount is used to update count.

We could have written setCount in one of two ways:

  • Basic usage: setCount(count + 1)
  • Using an updater function: setCount(prevCount => prevCount + 1)

We went with the second approach because in this example, the next state of count depends on its previous state. If the next state depends on the previous state, it is recommended to use the updater function that makes reference to the previous state.

Why not use a local variable instead?

Could we have used a local variable instead of useState? Let's find out.

const NoStateCounter = () => {
  let count = 0;

  const onClick = () => {
    count += 1;
  }  

  return (
    <div>
      <p>{count} clicks.</p>
      <button onClick={onClick}>
        Count
      </button>
    </div>
  );
}

The NoStateCounter component has a first initial render but it never re-renders because no component props or state ever changes. As a result, the count value changes when the button is clicked, but only when useState is used will the component re-render to show the current value of count. As a result, the component always displays 0 clicks even when the value of count is incremented.

Another pitfall to keep in mind when using local variables is that they get reset to their initial value on each component re-render, while useState variables do not. useState variables persist across re-renders.

Props and useState

If the data for a component is coming from props, we can use a local variable for it. If no computations are needed on a prop, we can also just reference the prop directly.

Using useState to put the prop in the state causes an unnecessary re-render. One re-render happens when the prop changes, and another re-render happens when the state is changed by the setter for useState.

import { useEffect, useState } from "react";

const ValueDisplay = ({ value }) => {
  const [countValue, setCountValue] = useState(0);

  // runs when the value prop changes
  useEffect(() => {
    // save latest value to state
    setCountValue(value);
  }, [value]);

  // for logging - runs on every component re-render
  useEffect(() => {
    console.log(`Rendered with value ${value}`);
  });

  return <p>{countValue}</p>;
};

const ValueChanger = () => {
  const [value, setValue] = useState(0);

  return (
    <div>
      <ValueDisplay value={value} />
      <button onClick={() => setValue(Math.random())}>Change Value</button>
    </div>
  );
}

In this example, we will get two lines of output in the console for every click of the "Change Value" button. This is because the ValueDisplay component will re-render twice. The first re-render will happen because the value prop changes due to new data passed along from ValueChanger. The second re-render will happen because there is a change to the component's state using setCountValue(value).

We actually do not need any useState for the ValueDisplay component. We can display the value prop directly. Since components re-render when a prop changes, the data displayed by ValueDisplay will always be in sync with the latest prop data.

Here's how we can fix and simplify the above example.

import { useState } from "react";

const ValueDisplay = ({ value }) => {
  return (
    <p>{value}</p>
  );
}

const ValueChanger = () => {
  const [value, setValue] = useState(0);

  return (
    <div>
      <ValueDisplay value={value} />
      <button onClick={() => setValue(Math.random())}>Change Value</button>
    </div>
  );
}

useState is asynchronous

React component state updates are not performed immediately when we call the setter function provided by useState. The setter function does not actually perform an immediate state update. Instead, it schedules a state update for some unknown time in the future. This means that we can't just look at the code to find out exactly when the state update occurred or will occur.

The useState setter function accepts a new state value and enqueues a re-render of the component. What does it mean to enqueue a re-render? It means that it doesn't re-render the component immediately. useState is asynchronous for performance reasons. This is why state changes with useState don't feel immediate.

const DoubleCount = () => {
  const [count, setCount] = useState(0);
  const [doubleCount, setDoubleCount] = useState(0);
  
  const handleCount = () => {
    setCount(count + 1);
    // this line won't use the latest value of count
    setDoubleCount(count * 2);    
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double Count: {doubleCount}</p>
      <button onClick={handleCount}>Count</button>
    </div>
  );
};

In this example, pressing the click button will update two local state variables. However, the doubleCount state is dependent on the count state. The value for doubleCount will always be one step behind count.

  • When count 1, doubleCount will be 0
  • When count is 2, doubleCount will be 2
  • When count is 3, doubleCount will be 4
  • When count is 4, doubleCount will be 6

By calling setDoubleCount right after setCount, we may have thought that setDoubleCount would use the latest value of count that was updated by setCount(count + 1). However, the value of count in setDoubleCount(count * 2), and in the entire handleCount function, will always be the count value before it has been incremented by setCount(count + 1).

The count references in the component will reflect the latest value of count only after the component has re-rendered by the state changes done in the handleCount function.

We can fix the above example by making use of the useEffect React hook with a dependency on count. This allows us to be sure that we are executing setDoubleCount(count * 2) only after count has actually been updated.

const DoubleCount = () => {
  const [count, setCount] = useState(0);
  const [doubleCount, setDoubleCount] = useState(0);

  useEffect(() => {
    setDoubleCount(count * 2);
  }, [count]);

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

Component re-renders

Whenever a React component uses any state via useState, it will need to be rendered again, or re-rendered, in response to any state change.

If a component does not re-render whenever state is updated, it will not be able to display any state updates to the user.

React schedules a component re-render every time the local state of a component changes. When a component re-renders, it will also cause all of its child components to re-render. This can make re-renders expensive computations that we may want to keep to a minimum.

Conclusion

In this post, we gained a solid understanding of useState, one of the most important hooks that every React developer must understand. State is a fundamental concept used by many React components and React applications. By knowing what state is, how to manage state with the useState hook, and how to avoid common useState pitfalls, we can write better React components.