George Rodier

Why useEffect and Objects Can Lead to Bugs

September 08, 2019

Just this last week I had to deal with a nasty bug in a web app I’m working on. Any time I performed an action on a page, an unexpected fetch call was being made that resulted in data I didn’t want appearing. After some investigation I was able to trace it to an object in the dependency array of useEffect.

TLDR: Since object comparison happens by reference, the objects in the dependency array of useEffect will always be different and cause the useEffect function to run. To solve, either make sure to use primitives in the dependency array OR wrap the object in a useMemo.

useEffect is one of the out of the box React hooks that allows you to run a side effect. A great example is running an asynchronous fetch and then setting some data in the useEffect. Robin Wieruch gives a great demonstration on how to achieve this in his blog post How to fetch data with React Hooks?. Here’s an example of what a fetch could look like from that article:

...
function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('redux');
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${search}`,
      );
      setData(result.data);
    };
    fetchData();
  }, [search]);
  return (
    ...
  );
}
export default App;

You’ll note the useEffect function takes two arguments. The first argument is a function to run. The second argument is an optional dependency array. The dependency array tells useEffect whether it should run the function passed in or not.

By default, if the second argument is not passed in, the function passed into useEffect will run every time the component is rendered. If there’s a dependency array but it’s empty ([]), the function passed into useEffect will only run once. However, if you include a value in dependency array, useEffect will compare that value to the previous render. If the value is the same it will skip the effect, but if the value is different it will run the effect.

Looking at the example above, the effect will only run if the value of search changes between renders. If search equals ‘redux’ on one render and also equals ‘redux’ on the next render, the effect will not run. However, if on the next render search is now equal to ‘flux’, the effect will now be run again, but with search having the value of ‘flux’.

And this brings us to dealing with the bug when using an object in the dependency array. The useEffect knows if a dependency changes by doing a comparison with Object.is(). A comparison between JavaScript primitives is what you would expect as it compares the primitives by value. However, in JavaScript, when two objects are compared, they are compared by reference rather than by values. So even if all the properties and the values in those properties are the same, if the object has a different reference, the two objects will not be equal.

const philadelphiaEaglesPlayers = {
  qb: 'Carson Wentz',
  te: 'Zach Ertz',
  wr: 'Alshon Jeffrey',
  rb: 'Miles Sanders',
};

const eaglesPlayers = {
  qb: 'Carson Wentz',
  te: 'Zach Ertz',
  wr: 'Alshon Jeffrey',
  rb: 'Miles Sanders',
};

// The values are the same but they have a different reference
Object.is(philadelphiaEaglesPlayers, eaglesPlayers); // false

// The values are the same and they have the same reference
// (point to the same object)
Object.is(eaglesPlayers, eaglesPlayers); // true

Now imagine an object with identical values being compared in the dependency array of useEffect. You may think that since the values are the same, the effect won’t run. However, since the objects are more than likely pointing to a different refernce, the comparison will fail and the function will run. This can lead to (as I mentioned at the top) nasty bugs where effects are being run when you’re not expecting them to. In this case, the effect will run every time even if all the values of the properties on the object don’t change.

So how do we solve this issue? There are usually two options I look to.

  1. Try to only use primitives (strings, numbers, etc) in the dependency array.
  2. If you absolutely need to use an object, wrap in in a useMemo hook

Strings, booleans, and numbers are all examples of primitives in JavaScript. The comparison for them will always be handled by value. So if you have an object, often times you really only want to run an effect if a particular property on that object changes. If that property is a primitive, use that instead of the object. Sometimes you may need to add a couple different properties of an object to the dependency array. This too can be a better option than using an object for comparison reasons.

If you are in a situation where you need to use an object in useEffect, and you’re sure there’s no better option, wrapping the object in the useMemo hook will allow you to compare the object against the same reference.

function App() {
  const someValue = useMemo(() => ({ value: "someValue" }), []);
  return (
    <>
      <Foo myObject={someValue} />
    </>
  );
}

function Foo({myObject}) {
  useEffect(() => {
    // some effect
  }, [myObject]);

  return (
    ...
  )
}

If someValue wasn’t wrapped in the useMemo, the Foo component would have a different object reference everytime it was rendered. However, useMemo returns a memoized value. Unless that changes depending on the dependency array for useMemo, it will always return the reference to the same object. As a result the comparison for the effect in Foo will run as expected. That said, when given the option, try to prefer using primitives to useMemo or useCallback (which will do something similar with functions).

Obviously there are a lot of subtleties with hooks which has caused some reservations among those trying to adopt them. But when understood and used correctly, they provide a powerful way to share functionality between components. To read more about useEffect and other hooks, I’d definitely recommend checking out the following articles:


Join the Newsletter

Follow along for the latest updates on my side projects, blog posts, and more!

    I promise not to send spam, but feel free to unsubscribe at any time!

    George Rodier

    A personal site for George Rodier, a software developer living in Philadelphia and building web applications for a living.