Stop useEffect from running on every render with useCallback

Max Rozen
@RozenMD

What happens if you add a function to your useEffect's dependency array, and suddenly start to get infinite re-renders?

Chances are, you've run in to the perfect opportunity to use 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.

This means even when your component re-renders, you can be sure your function wrapped in useCallback won't be re-declared, preventing the dreaded infinite re-render/useEffect loop.

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.

Hey! Tired of infinite re-renders when using useEffect?

I've started teaching a free mini-course on useEffect via email. I'd love to hear what you think!

You'll learn how to fetch data with useEffect, how to use the dependency array, even how to prevent infinite re-renders with useCallback.

    Join 1,555 React developers that have signed up so far!