25

I have Function Component that utilizes hooks. In the useEffect hook, I simply want to fetch data from my back end and store the results in state. However, despite adding the data variable as a dependency, useEffect still fires on an infinite loop - even though the data hasn't changed. How can I stop useEffect from firing continuously?

I've tried the empty array hack, which DOES stop useEffect from continuously firing, but it's not the desired behavior. If the user saves new data, for example, useEffect should fire again to get updated data - I'm not looking to emulate componentDidMount.

const Invoices = () => {
  const [invoiceData, setInvoiceData] = useState([]);

  useEffect(() => {
    const updateInvoiceData = async () => {
      const results = await api.invoice.findData();
      setInvoiceData(results);
    };
    updateInvoiceData();
  }, [invoiceData]);

  return (
    <Table entries={invoiceData} />
  );
};

I expected useEffect to fire after the initial render, and again ONLY when invoiceData changes.

dance2die
  • 35,807
  • 39
  • 131
  • 194
Trevor
  • 423
  • 1
  • 4
  • 10
  • Have a look at my explanation here https://stackoverflow.com/questions/57847626/using-async-await-inside-a-react-functional-component/57856876#57856876 – Milind Agrawal Sep 09 '19 at 18:38
  • Isn't this the same behavior as an empty dependency array? if (!invoiceData) updateInvoiceData(); – Trevor Sep 09 '19 at 18:42
  • It will keep on getting called if there is some value in `invoiceData`. The second argument in `useEffect` will handle that better and will only execute the code when value in `invoiceData` is changed. But if the value is getting changed every time you are making a call then this won't work. – Milind Agrawal Sep 09 '19 at 18:48
  • I would suggest the move the `updateInvoiceData` logic to separate method and use`useEffect` only for the first time and then whenever new data is saved, call the method again(Do not use `useEffect`) – Milind Agrawal Sep 09 '19 at 18:50
  • @Trevor How did you solve this? I have similar problem. The most voted answer pointed out the problem but offer no solution. – UMR Oct 20 '21 at 08:18

4 Answers4

35

The way the useEffect dependency array works is by checking for strict (===) equivalency between all of the items in the array from the previous render and the new render. Therefore, putting an array into your useEffect dependency array is extremely hairy because array comparison with === checks equivalency by reference not by contents.

const foo = [1, 2, 3];
const bar = foo;
foo === bar; // true

const foo = [1, 2, 3];
const bar = [1, 2, 3];
foo === bar; // false

Inside of your effect function, when you do setInvoiceData(results) you are updating invoiceData to a new array. Even if all the items inside of that new array are exactly the same, the reference to the new invoiceData array has changed, causing the dependencies of the effect to differ, triggering the function again -- ad infinitum.

One simple solution is to simply remove invoiceData from the dependency array. In this way, the useEffect function basically acts similar to componentDidMount in that it will trigger once and only once when the component first renders.

useEffect(() => {
    const updateInvoiceData = async () => {
      const results = await api.invoice.findData();
      setInvoiceData(results);
    };
    updateInvoiceData();
  }, []);

This pattern is so common (and useful) that it is even mentioned in the official React Hooks API documentation:

If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.

jered
  • 11,220
  • 2
  • 23
  • 34
  • That works when the exhaustive deps is turned off but when that is required? – wattry Dec 21 '20 at 23:10
  • @Ryan `// eslint-disable-next-line react-hooks/exhaustive-deps` – jered Dec 23 '20 at 00:21
  • that leaves room for bugs, is there not a better solution? I've seen some implement useRefs. – wattry Dec 23 '20 at 14:40
  • 1
    @Ryan What bugs do you mean? Ultimately it is up to the programmer to understand how useEffect and the dependency array works and utilize them properly. There is no way for eslint or any other static analysis tool to “know” whether you intend a useEffect hook to act like a “componentDidMount” or not and lint accordingly. If you’re worried about dropping too many `eslint-disable` comments, you could write a `useOnMount()` custom hook for yourself which simply wraps the desired behavior (empty deps array, cleanup and eslint comment). – jered Dec 23 '20 at 20:51
3

Credit to jered for the great "under the hood" explanation; I also found Milind's suggestion to separate out the update method from useEffect to be particularly fruitful. My solution, truncated for brevity, is as follows -

const Invoices = () => {
  const [invoiceData, setInvoiceData] = useState([]);

  useEffect(() => {    
    updateInvoiceData();
  }, []);

  // Extracting this method made it accessible for context/prop-drilling
  const updateInvoiceData = async () => {
    const results = await api.invoice.findData();
    setInvoiceData(results);
  };

  return (
    <div>
      <OtherComponentThatUpdatesData handleUpdateState={updateInvoiceData} />
      <Table entries={invoiceData} />
    </div>
  );
};
Trevor
  • 423
  • 1
  • 4
  • 10
1

What's happening, is that when you update the invoiceData, that technically changes the state of invoiceData, which you have watched by the useEffect hook, which causes the hook to run again, which updates invoiceData. If you want useEffect to run on mount, which I suspect, then pass an empty array to the second parameter of useEffect, which simulates componentDidMount in class components. Then, you'll be able to update the local UI state with your useState hook.

Stephen Collins
  • 845
  • 7
  • 12
  • +1. This would be the good example when we can "fake" the deps with `[]` as updating `invoiceData` would trigger re-render, which runs effect, then sets `invoiceData` agains, etc... – dance2die Sep 09 '19 at 18:43
1

I totally agree with Jared's answer. But for some scenarios where you really want to not have reference comparison, then useDeepCompareEffect from react-use library is really good https://github.com/streamich/react-use/blob/HEAD/docs/useDeepCompareEffect.md