Articles

React Hooks: useState

5 min read

In this series of blog posts, I do a deep dive into every React hook

Syntax

The useState hook is the most simple and commonly used React hook. It's used to store a single state variable and is used like so:

import { useState } from 'react'
const [count, setCount] = useState(0)

As you can see, the useState hook returns an array with two values. We're able to assign these values into count and setCount variables using the destructuring assignment syntax. The following code, while slightly more verbose, will achieve the same result as above:

const state = useState(0)
const count = state[0]
const setCount = state[1]

It's also important to note that we can name count and setCount to whatever we want, but convention suggests to use <variable> and set<Variable>.

Return Values

The first value in the array, which we're assigning to count is a single state variable.

The second value, setCount, is a function that sets the value of the state variable. If we wanted to change the count to 2 we would invoke the function as setCount(2).

Here's an example of using the setCount() function to increment and decrement the state variable using two buttons.

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  return (
    <>
      <div>The count is {count}</div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </>
  )
}

Open browser consoleConsole

Setting the Same Value

Whenever the setCount() function is called with a new value, the component will re-render in order to display the updated value. Note that this will only happen if the value passed to setCount() is different from the current one.

import { useState } from 'react'

export default function Counter() {
  console.log('component rerendered')
  const [count, setCount] = useState(5)

  return (
    <>
      <div>The count is {count}</div>
      <button onClick={() => setCount(5)}>Set count to same value</button>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </>
  )
}

Open browser consoleConsole

Setting State With a Function

The setter function returned by useState can also take in a function where the argument is the previous state value:

setCount((previousCount) => previousCount + 1)

This is actually good practice when setting the state based on a previous value (even though I didn't do it in my other examples 😬). The reason why this is good practice is that there can be unexpected state updates if there is a delay between the function call and the call to the setter. This is a bit complicated, so here's an example to better illustrate.

Kent C Dodd's has a great blog post about this that the following example is derived from

In the example below, if you press the "Increment" button quickly three times (within the 500ms limit), you'll notice that the count only increments by one.

import { useState } from 'react'

export default function DelayedCounter() {
  const [count, setCount] = useState(0)
  const increment = async () => {
    await new Promise(resolve => setTimeout(resolve, 500))
    setCount(count + 1)
  }
  return (
    <>
      <div>The count is {count}</div>
      <button onClick={increment}>Increment</button>
    </>
  )
}

Open browser consoleConsole

In order to rectify this problem, we can use setCount(previousCount => previousCount + 1). Try adding this to the example above to ensure that the button clicks increment the count correctly.

Arguments

The useState hook takes in a single argument that sets the initial value of the state variable on first render. It can also, however, take in a function. If a function is passed it will lazily initialize the initial value.

Lazy Initialization

Lazy initialization is helpful when the initial value passed into the useState() hook is computationally expensive. To illustrate this concept, assume we have the following component.

import { useState } from 'react'

function expensiveComputation() {
  console.log("expensive computation ran")
  return 0
}

export default function Counter() {
  const [count, setCount] = useState(expensiveComputation())
  return (
    <>
      <div>The count is {count}</div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </>
  )
}

Open browser consoleConsole

Even though the initial value gets disregarded on subsequent renders, the function that creates the initial value still runs. This can be problematic if we have initial values that are expensive to calculate because they can cause a significant delay when rendering our components.

To calculate the initial value on only the first render and not on subsequent renders, we can pass a function to the useState hook.

import { useState } from 'react'

function expensiveComputation() {
  console.log("expensive computation ran")
  return 0
}

export default function Counter() {
  const [count, setCount] = useState(() => expensiveComputation())
  return (
    <>
      <div>The count is {count}</div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </>
  )
}

Open browser consoleConsole

If you check the logs in the example above, you'll see that even though the component re-renders, the initialization function does not get called.

Note that you should probably only use this if you're noticing significant render delays in your components, otherwise it's best practice to pass the value without lazy initialization.

© Matt Beiswenger 2023Built with Next.js, deployed on ▲Vercel