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
useState
hook.- 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
useState
hook, 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
useState
variable, move theuseState
hook 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
useMemo
hook 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
useReducer
hook instead.- You’ll otherwise need to be super careful around which
setState
you set first, as that will immediately force a re-render (unless you’re on React v18). - Alternatively, use
setState
with a combined object. This is a bit annoying though, as you’ll need to be sure all keys are set, every timesetState
is called.
- You’ll otherwise need to be super careful around which
useEffect
useEffect
should be used to sync your state with something outside of React (e.g. network, DOM).- This means that if you're using
useEffect
to 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
useEffect
either 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
useEffect
is 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
useEffect
to call, do it inside the effect callback, not outside. You’ll otherwise need to be careful to wrap your function in auseCallback
and adding it to theuseEffect
dependency array.- Additionally, if you need to control the abortion of the function via the return function of
useEffect
, you’ll need to useuseRef
to 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
useCallback
is because every timeuseEffect
re-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.useLayoutEffect
is useful however, if your effect mutates the DOM and visibly changes the appearance, causing screen flickering. useEffect
runs asynchronously AND after a render is painted to the screen, whereasuseLayoutEffect
runs synchronously after a render but before the screen is updated.