Practical Tips When Using React’s Two Most Common Hooks
5 min read
The two most common hooks in React that you’ll be using continuously are useState and useEffect. Much has been written about how they work, so I won’t go into the basic mechanics (you can read about them in the official React docs for useState and useEffect). What I do want to talk about instead, are things to keep in mind when using these two hooks, to ensure you’re not inadvertently causing unnecessary renders or worst still, infinite loops.
Before kicking off, remember that React updates are caused by parent re-renders, state changes and props changes (including context changes which act as a form of internalised state).
useState
- First ask yourself whether what you need is truly the
useStatehook.- Can you use a standard variable? State should only be used to persist data between component re-renders.
- If your variable needs to be calculated during render time, you should not be using
useState.
- If you’re going to update state with
useState, do not mutate the original state object to avoid weird mutation and synchronisation issues.
const [toggle, setToggle] = useState(false)
// Do this
<button onClick={() => setToggle(prevState => !prevState)}>Toggle</button>
// Not this
<button onClick={() => setToggle(!toggle)}>Toggle</button>- If you have an expensive computation for setting the initial value in a
useStatehook, pass in a function instead of the function value. Read more about lazy internal state. - Think about colocating state i.e. placing state as close as possible to where it’s actually being used. e.g. if you find that only one child component needs that
useStatevariable, move theuseStatehook down from the parent to the variable so that React doesn’t need to check that variable every time the parent re-renders. - Not all state needs to be managed by
useState. You can probably avoid a lot of synchronisation issues between various pieces of state by using derived state instead (i.e. calculating dependent state, instead of usingsetState). Read more about it in Kent’s blog post.- If you’re worried about performance being an issue, you shouldn’t because it probably isn’t. 😊 But if it really is, use the
useMemohook instead to wrap computationally expensive derived state calculations. Read more about useMemo in my previous blog post.
- If you’re worried about performance being an issue, you shouldn’t because it probably isn’t. 😊 But if it really is, use the
- If you don’t want to use derived state, and if you have two or more bits of state that rely on each other, consider using a
useReducerhook instead.- You’ll otherwise need to be super careful around which
setStateyou set first, as that will immediately force a re-render (unless you’re on React v18). - Alternatively, use
setStatewith a combined object. This is a bit annoying though, as you’ll need to be sure all keys are set, every timesetStateis called.
- You’ll otherwise need to be super careful around which
useEffect
useEffectshould be used to sync your state with something outside of React (e.g. network, DOM).- This means that if you're using
useEffectto sync two pieces of state, you're probably doing it wrongly (see derived state point above).
- This means that if you're using
- Think of
useEffecteither as synchronising input, or synchronising output. Do not mix the two, or you'll start running into infinite loops.- An example of synchronising input: call external API, receive data, set state.
- An example of synchronising output: something changes, update the DOM.
- The only thing you can return from
useEffectis the cleanup function. This means you cannot directly await for an async function, as that implicitly/explicitly returns a promise. We’ll need to do this instead:
// Do this:
React.useEffect(() => {
async function myAsyncFunction() {
const result = await somethingAsync()
// take result and do something with it
}
myAsyncFunction()
})
// Alternatively:
React.useEffect(() => {
somethingAsync().then((result) => {
// take result and do something with it
})
})
// NOT:
React.useEffect(async () => {
const result = await somethingAsync()
// take result and do something with it
})- If you have to define a function for your
useEffectto call, do it inside the effect callback, not outside. You’ll otherwise need to be careful to wrap your function in auseCallbackand adding it to theuseEffectdependency array.- Additionally, if you need to control the abortion of the function via the return function of
useEffect, you’ll need to useuseRefto be able to reference the abort function.
- Additionally, if you need to control the abortion of the function via the return function of
- The reason you want to be sure to wrap the function definition in a
useCallbackis because every timeuseEffectre-renders, it creates a new version of the function, which triggers theuseEffect, which creates a new version of the function etc.
useLayoutEffect
- This is similar to
useEffect, which is what you’ll be using the vast majority of the time.useLayoutEffectis useful however, if your effect mutates the DOM and visibly changes the appearance, causing screen flickering. useEffectruns asynchronously AND after a render is painted to the screen, whereasuseLayoutEffectruns synchronously after a render but before the screen is updated.