DEV Community

loading...
Cover image for Things to know about useState

Things to know about useState

tkdodo profile image Dominik D Originally published at tkdodo.eu ・5 min read

Note:

Some examples are interactive on my blog, so you might have a better experience reading it there:

Things to know about useState


React.useState is pretty straightforward to use. A value, a setter function, an initial state. What hidden gems could possibly be there to know about? Well, here are 5 things you can profit from on a daily basis that you might not have known:

1: The functional updater

Good old setState (in React class components) had it, and useState has it, too: The functional updater! Instead of passing a new value to the setter that we get from useState, we can also pass a function to it. React will call that function and gives us the previousValue, so that we can calculate a new result depending on it:

const [count, setCount] = React.useState(0)

// 🚨 depends on the current count value to calculate the next value
<button onClick={() => setCount(count + 1)}>Increment</button>

// ✅ uses previousCount to calculate next value
<button onClick={() => setCount(previousCount => previousCount + 1)}>Increment</button>
Enter fullscreen mode Exit fullscreen mode

This might be totally irrelevant, but it might also introduce subtle bugs in some situations:

Calling the same setter multiple times

Example:

function App() {
    const [count, setCount] = React.useState(0)

    return (
        <button
            onClick={() => {
                setCount(count + 1)
                setCount(count + 1)
            }}
        >
            🚨 This will not work as expected, count is: {count}
        </button>
    )
}
Enter fullscreen mode Exit fullscreen mode

Each click will only increment the count once, because both calls to setCount closure over the same value (count). It's important to know that setCount will not immediately set the count. The useState updater only schedules an update. It basically tells React:

Please set this value to the new value, somewhen.

And in our example, we are telling React the same thing twice:

Please set the count to two
Please set the count to two

React does so, but this is probably not what we intended to say. We wanted to express:

Please increment the current value
Please increment the current value (again)

The functional updater form ensures this:

function App() {
    const [count, setCount] = React.useState(0)

    return (
        <button
            onClick={() => {
                setCount((previousCount) => previousCount + 1)
                setCount((previousCount) => previousCount + 1)
            }}
        >
             Increment by 2, count is: {count}
        </button>
    )
}
Enter fullscreen mode Exit fullscreen mode

When async actions are involved

Kent C. Dodds has written a lengthy post about this here, and the conclusion is:

Any time I need to compute new state based on previous state, I use a function update.

— Kent C. Dodds

I can second that conclusion and encourage you to read that article thoroughly.

Bonus: Avoiding dependencies

The functional updater form can also help you to avoid dependencies for useEffect, useMemo or useCallback. Suppose you want to pass an increment function to a memoized child component. We can make sure the function doesn't change too often with useCallback, but if we closure over count, we will still create a new reference whenever count changes. The functional updater avoids this problem altogether:

function Counter({ incrementBy = 1 }) {
    const [count, setCount] = React.useState(0)

    // 🚨 will create a new function whenever count changes because we closure over it
    const increment = React.useCallback(() => setCount(count + incrementBy), [
        incrementBy,
        count,
    ])

    // ✅ avoids this problem by not using count at all
    const increment = React.useCallback(
        () => setCount((previousCount) => previousCount + incrementBy),
        [incrementBy]
    )
}
Enter fullscreen mode Exit fullscreen mode

Bonus2: Toggling state with useReducer

Toggling a Boolean state value is likely something that you've done once or twice before. Judging by the above rule, it becomes a bit boilerplate-y:

const [value, setValue] = React.useState(true)

// 🚨 toggle with useState
<button onClick={() => setValue(perviousValue => !previousValue)}>Toggle</button>
Enter fullscreen mode Exit fullscreen mode

If the only thing you want to do is toggle the state value, maybe even multiple times in one component, useReducer might be the better choice, as it:

  • shifts the toggling logic from the setter invocation to the hook call
  • allows you to name your toggle function, as it's not just a setter
  • reduces repetitive boilerplate if you use the toggle function more than once
// ✅ toggle with useReducer
const [value, toggleValue] = React.useReducer(previousValue => !previousValue, true)

<button onClick={toggleValue}>Toggle</button>
Enter fullscreen mode Exit fullscreen mode

I think this shows quite well that reducers are not only good for handling "complex" state, and you don't need to dispatch events with it at all costs.

2: The lazy initializer

When we pass an initial value to useState, the initial variable is always created, but React will only use it for the first render. This is totally irrelevant for most use cases, e.g. when you pass a string as initial value. In rare cases, we have to do a complex calculation to initialize our state. For these situations, we can pass a function as initial value to useState. React will only invoke this function when it really needs the result (= when the component mounts):

// 🚨 will unnecessarily be computed on every render
const [value, setValue] = React.useState(calculateExpensiveInitialValue(props))

// ✅ looks like a small difference, but the function is only called once
const [value, setValue] = React.useState(() => calculateExpensiveInitialValue(props))
Enter fullscreen mode Exit fullscreen mode

3: The update bailout

When you call the updater function, React will not always re-render your component. It will bail out of rendering if you try to update to the same value that your state is currently holding. React uses Object.is to determine if the values are different. See for yourself in this example:

function App() {
    const [name, setName] = React.useState('Elias')

    // 🤯 clicking this button will not re-render the component
    return (
        <button onClick={() => setName('Elias')}>
            Name is: {name}, Date is: {new Date().getTime()}
        </button>
    )
}
Enter fullscreen mode Exit fullscreen mode

4: The convenience overload

This one is for all TypeScript users out there. Type inference for useState usually works great, but if you want to initialize your value with undefined or null, you need to explicitly specify the generic parameter, because otherwise, TypeScript will not have enough information:

// 🚨 age will be inferred to `undefined` which is kinda useless
const [age, setAge] = React.useState(undefined)

// 🆗 but a bit lengthy
const [age, setAge] = React.useState<number | null>(null)
Enter fullscreen mode Exit fullscreen mode

Luckily, there is a convenience overload of useState that will add undefined to our passed type if we completely omit the initial value. It will also be undefined at runtime, because not passing a parameter at all is equivalent to passing undefined explicitly:

// ✅ age will be `number | undefined`
const [age, setAge] = React.useState<number>()
Enter fullscreen mode Exit fullscreen mode

Of course, if you absolutely have to initialize with null, you need the lengthy version.

5: The implementation detail

useState is (kinda) implemented with useReducer under the hood. You can see this in the source code here. There is also a great article by Kent C. Dodds on how to implement useState with useReducer.

Conclusion

The first 3 of those 5 things are actually mentioned directly in the Hooks API Reference of the official React docs I linked to at the very beginning 😉. If you didn't know about these things before - now you do!


How many of these points did you know? Leave a comment below ⬇️

Discussion (4)

pic
Editor guide
Collapse
awhite profile image
Alex White

Did not know about the lazy initializer. Will definitely be using that more in the future

Collapse
mbrtn profile image
Ruslan Shashkov

This is an antipattern anyway. Better to useMemo with useEffect in case of expensive calculations

Collapse
tkdodo profile image
Dominik D Author

Not sure what you mean with that. Do you have an example maybe?

Collapse
yangpengyao profile image
YangPengYao

Learn a lot !!
Really good articles !!!