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() )