Web Dev Simplified Blog

Everything You Need To Know About useState

April 20, 2020

One of the most important parts of any application is managing state. Most code that is written in some way deals with modifying or reading state, so understanding how to manage state is incredibly important. Before hooks were introduced the only way to modify state was with class components and this.state, but React has introduced hooks, specifically the useState hook, which is a new way to handle state inside of function components. There are a few differences between state and function component state management, so in this article I will be explaining everything you need to know about useState so you can start building stateful function components.

If you prefer to learn visually, check out the video version of this article.

From Classes To Functions

In order to understand how the useState hook works we first need to look at how state is managed in class components. For this article we are going to use a simple counter component for all examples.

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

  changeCount(amount) {
    this.setState(prevState => {
      return { count: prevState.count + amount }
    })
  }

  resetCount() {
    this.setState({ count: 0 })
  }

  render() {
    return (
      <>
        <span>{this.state.count}</span>
        <button onClick={() => this.changeCount(1)}>+</button>
        <button onClick={() => this.changeCount(-1)}>-</button>
        <button onClick={() => this.resetCount()}>Reset</button>
      </>
    )
  }
}

Essentially all this component does is create a counter with a plus, minus, and reset button and by default the counter has a value of 0 when it first renders. Now let’s look at how we can convert this class component to a function component with useState. To start with we will use the following base code.

function Counter() {
  // TODO: Create state

  function changeCount(amount) {
    // TODO: Update state
  }

  function resetCount() {
    // TODO: Update state
  }

  return (
    <>
      <span>{/* TODO: Show State */}</span>
      <button onClick={() => changeCount(1)}>+</button>
      <button onClick={() => changeCount(-1)}>-</button>
      <button onClick={() => resetCount()}>Reset</button>
    </>
  )
}

Creating initial state

In the class component example the initial state is defined in a constructor as an object which contains all the state for the component.

constructor(props) {
  super(props)
  this.state = { count: 0 }
}

Obviously function components do not have constructors, so instead the useState hook takes the initial state as an argument.

useState(initialState)

The useState hook also returns an array with two entries. The first entry in the array is the current state while the second entry is the method which allows us to update the state.

const [state, setState] = useState(initialState)

In order to easily breakout the array that is returned from useState we are using destructuring. If you are not familiar with destructuring you can checkout this video tutorial on the topic.

If we were to map the class component directly to useState we would end up with something like this.

const [state, setState] = useState({ count: 0 })

Now technically there is nothing wrong with this, but since function components can use multiple useState hooks inside one component it is much more common to have an individual useState hook for each piece of state. In our example, that would mean we would have a single useState hook for managing just the count state.

const [count, setCount] = useState(0)

This allows us to simplify out component state out into their own variables instead of cramming all the state into one big this.state object. Let’s plug this into our function component.

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

  function changeCount(amount) {
    // TODO: Update state
  }

  function resetCount() {
    // TODO: Update state
  }

  return (
    <>
      <span>{count}</span>
      <button onClick={() => changeCount(1)}>+</button>
      <button onClick={() => changeCount(-1)}>-</button>
      <button onClick={() => resetCount()}>Reset</button>
    </>
  )
}

Updating State

Updating state with function components is luckily very similar to updating state with class components. The main difference is that in function components you will be updating a single state variable, like count, while in class components you call this.setState on the entire state object. For example, in our class component to reset the state we have the following code.

this.setState({ count: 0 })

This will update the count portion of this.state to be 0. With hooks, since all of the state is broken out into their own variables we can use the setCount function to directly set the count state.

setCount(0)

With this approach we do not have to worry about creating a new object just to set the state and instead can just set the state directly to the value we want.

Inside the changeCount function we are using the second form of this.setState to update the count based on the previous count.

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

This code is pretty clunky since it involves creating a new object from values of the previous object, but with function components, since we are not using an object in state this code is much cleaner to write.

setCount(prevCount => prevCount + amount)

As you can see we are able to just directly set the count to a value instead of having to create a brand new object to handle it for us.

Let’s add the code for updating of our state into our function component.

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

  function changeCount(amount) {
    setCount(prevCount => prevCount + amount)
  }

  function resetCount() {
    setCount(0)
  }

  return (
    <>
      <span>{count}</span>
      <button onClick={() => changeCount(1)}>+</button>
      <button onClick={() => changeCount(-1)}>-</button>
      <button onClick={() => resetCount()}>Reset</button>
    </>
  )
}

With that we now have a complete counter component written using a function component.

useState Gotchas

There are a few extra things about useState which are a bit different than class component state that you need to be aware of.

Updating State Objects

I mentioned earlier that most of the time you will use single values with useState, but there are some cases where using an object makes more sense. Let’s just use the example of user preferences.

const [preferences, setPreferences] = useState({
  theme: "light",
  fontSize: "normal",
})

If you are used to class components then when you decide to update the theme portion of this state you may think to do something like this.

setPreferences({ theme: "dark" })

This is wrong, though. What this code does is update the entire preferences object to be just { theme: 'dark' } without any fontSize. This is because the set method from useState will overwrite the entire value of the state with the new value, so the new value of { theme: 'dark' } overwrites all of the old state. In order to make sure this does not happen you would need to combine the old state with the new state manually.

setPreferences(prevPreferences => {
  return { ...prevPreferences, theme: "dark" }
})

This code will combine all the old preferences with the new dark theme preference.

Initial State Computation

Sometimes it is slow to compute the initial state of a component. This is not a problem in class components since the initial state computation only happens once in the constructor, but in function components the initial state computation is declared in the render function and happens every render. Having a slow initial state computation can slow down an entire application significantly because of this.

useState(/* Slow computation */)

Luckily, useState can also take a function as the argument instead of a value, and that function will only be run the very first time a component is rendered. By using this function version of useState you will no longer run the slow computation each render, but only once on the first render of the component just like class components.

useState(() => {
  /* Slow computation */
})

Using Multiple useState Hooks

As I mentioned previously, you should break out your state into individual useState hooks. This is as simple as having multiple useState hooks one after another in the code.

const [count, setCount] = useState(0)
const [color, setColor] = useState("red")

This is really nice since it makes sure each part of your state has its own name and update function which makes handling state much easier.

Conclusion

Overall, useState is not much different than class component state, which makes switching to useState quite painless. There are only a few small gotchas to worry about, but most likely you will never run into these issues.