Max RozenMax Rozen
TwitterArticlesNewsletter

Stop useEffect from running on every render with useCallback

If you’ve done much searching to try understand why your useEffect runs on every render, chances are you’ve run into useCallback.

The syntax is:

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

useCallback returns you a new version of your function only when its dependencies change. In the example above, that’s only when a or b changes.

Before we go too far with useCallback, let’s have a quick refresher on React components.

Quick refresher

Here’s a React component that renders a child component. Occassionally, something will happen that will make the component re-render (such as props changing, or useState being called).

function SomeComponent(props) {
  //do some other stuff here that might cause re-renders, like setting state
  return <DataFetcher />;
}

Now, if we wanted to pass <DataFetcher/> a prop, such as a function that generates a URL to fetch data from, you might define a function as follows:

function SomeComponent(props){
  function getUrl(id){    return "https://some-api-url.com/api/" + id + "/"  }  //do some other stuff here that might cause re-renders, like setting state
  return <DataFetcher getUrl={getUrl}>
}

Before we had to worry about hooks, we could define a function in a class, and pass it down to children that could fire off this.getUrl(id) without a worry. However, now that we’re in a function component, the way we’ve defined getUrl means something different.

Since by default all of SomeComponent’s code re-runs on render, getUrl gets re-defined every render.

If DataFetcher then uses getUrl as part of a useEffect hook, even if you add getUrl to the dependency array, your useEffect would fire every single render.

useEffect(() => {
  fetchDataToDoSomething(getUrl);
}, [getUrl]); // 🔴 re-runs this effect every render

How do we stop useEffect from running every render?

Back in SomeComponent, we have two options to deal with this (assuming we can’t just move getUrl into the troublesome useEffect hook).

  1. The old fashioned way: move getUrl outside of the component, so it doesn’t get re-declared every render:

    function getUrl(id){  return "https://some-api-url.com/api/" + id + "/"}
    function SomeComponent(props){
      //do some other stuff here that might cause re-renders, like setting state
      return <DataFetcher getUrl={getUrl}>
    }
  2. The Hooks way, which is to wrap getUrl in a useCallback:

    function SomeComponent(props){
      const getUrl = useCallback(function (id) {    return "https://some-api-url.com/api/" + id + "/";  }, []); // <-- Note that we can't add id to the deps array in this case
      //do some other stuff here that might cause re-renders, like setting state
      return <DataFetcher getUrl={getUrl}>
    }

    Of course, this is a pretty simplified example to illustrate what you could do to fix much more complicated code.

    I’m not suggesting you should pass a function that returns a string in your real code, when you could pass the string, and avoid using useCallback/useMemo everywhere.


The key takeaway here is that useCallback returns you a new version of your function only when its dependencies change, saving your child components from automatically re-rendering every time the parent renders. This is particularly useful when used with useEffect, as we can safely add functions wrapped in useCallback to the dependency array without fear of infinite re-renders.

Do you struggle to keep up with best practices in React?

I send a single email weekly with an article like this one to help improve the quality of your React apps. Lots of developers like them, and I'd love to hear what you think as well. You can always unsubscribe.

    Join 158 React developers who signed up last month!

    rssTwitterGitHub

    © 2020 Max Rozen. All rights reserved.