Avatar

Nǐ hǎo, I'm Julia.

Practical Tips When Using React’s Two Most Common Hooks

#react

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 the useState 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 using setState). 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 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 time setState is called.

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).
  • 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 a useCallback and adding it to the useEffect dependency array.
    • Additionally, if you need to control the abortion of the function via the return function of useEffect, you’ll need to use useRef to be able to reference the abort function.
  • The reason you want to be sure to wrap the function definition in a useCallback is because every time useEffect re-renders, it creates a new version of the function, which triggers the useEffect, 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, whereas useLayoutEffect runs synchronously after a render but before the screen is updated.

© 2016-2024 Julia Tan · Powered by Next JS.