Alex's Notes

useState (McGinnis)

Topic 2 of React Hooks: Tyler McGinnis

The normal mental model for useState is that it allows us to add state to functional components, but this isn’t the best way of thinking about it. More important is that it allows us to preserve values between renders (function calls), and easily trigger a re-render of the component.

Usually with functions, unless you’re using closures the values in a function will be disposed when the function has executed. Each subsequent call to the function will then create its own values anew.

When talking about UI components though, this has limitations. If React components are functions, and functions dispose of their values after execution, how is that going to work? Under the hood React needs some means of preserving a value. The public API for that means, is useState.

So when useState is run the first time, ie when the component function is first executed, it takes the initial value you pass to it. But React tracks that execution and next time the function is called (next time the component is rendered), it does not take its value from that parameter, it preserves the previous value from the last execution of the function.

The other aspect is that setting the state with useState triggers a re-render, ie triggers the function to be executed again with the new state. This is in contrast with useRef which allows us to update state without re-rendering the component.

Differences from the old API

So how does this compare with the old setState API on class components?

There is now no instance-wide API for updating the whole state of a component. Each piece of state has its own updater function.

Likewise there is no instance wide state object that you set on the component. Each piece of state has its own useState invocation.

The other difference is how they update objects.

In the old days you’d have a state object with a bunch of properties. If you called this.setState({loading}) you’d only modify the loading property on that object, the rest would be ignored.

That’s not true any more. Since each piece of state is unique, when you call setProp under the useState API you replace it entirely.

What this means is that if your piece of state is best modelled as an object, you should look to the useReducer hook instead.

The other difference is functional updates. When you wanted to update state based on the old state (like increment a count). You would have to pass a function to the old setState method, since it was async. To guarantee correctness you couldn’t rely on the state in the component instance at the time, you had to pass a function that took the state and returned a new state, so React would use the state at the time.

This works the same way in the new API. So if you want to increment a count you should do this:


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

const increment = () => setCount((count) => count + 1);

Lazy Evalution

One last gotcha on useState.

We’ve said that the initial value you pass to useState is only used once, the first time the component is rendered.

However, if you pass an expression that is costly to evaluate note that this will still evaluate even if the result is ignored on subsequent renders of the component.

So, to avoid this, you need to pass a function to useState. Then React will not execute the function on subsequent renders.

IE don’t do this:

const [ myState, setMyState ] = React.useState( expensive() )

Do do this:

const [ myState, setMyState ] = React.useState( () => expensive() )

Links to this note