0

I made a custom hook that fetches a News API and returns a handler for loading, errors and data (inspired by Apollo Client). The problem is that when using it, it will fire itself infinitely, even though the items in the dependency array don't change. This is how I'm implementing it:

The hook:

const useSearch = (query: string, sources: string[]) => {
  const [response, setResponse] = useState<State>({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    newsapi
      .getEverything({
        q: query,
        pageSize: 1,
        sources: sources,
      })
      .then((data) => {
        setResponse({ data, loading: false, error: null });
      })
      .catch((e) => {
        setResponse({ data: null, loading: false, error: e });
      });
  }, [query, sources]);

  return response;
};

Usage:

  const { loading, error, data } = useSearch("Donald", ["bbc-news"]);

And I exceeded my daily rate for the API:

enter image description here

What am I doing wrong?

martin
  • 887
  • 7
  • 23
  • 1
    useEffect dependency array is a reference comparison, the array passed to useSearch is recreated each time. Use something like a useMemo to store the value of the array before passing it to your useSearch to ensure that it is passed the same reference value between rerenders. – Jacob Smit Dec 03 '20 at 01:51
  • Check where you're calling useSearch hook. If you're calling it unconditionally it will cause infinite loop because it is called on each render. – aleksa_95 Dec 03 '20 at 02:01
  • 1
    you just need to change one thing: `useEffect(() => { }, [query, ...sources])` – hackape Dec 03 '20 at 02:02
  • @hackape Your solution worked! Why did we need to copy the `sources` array? – martin Dec 03 '20 at 02:12
  • @JacobSmit Just copying the array with the three dots made the trick, thanks! – martin Dec 03 '20 at 02:16
  • 1
    he is not copying the sources, he is spreading the sources in the dependency array. This makes react see N number of strings to compare (which are compared by value not reference) instead of an array (which is compared by reference) removing the need to provide a memoized or constant array. – Jacob Smit Dec 03 '20 at 02:20
  • oh, I see. Thanks for the explanation. As a python guy it's hard to transition into these new ES6 things – martin Dec 03 '20 at 02:22
  • Does this answer your question? [Infinite loop in useEffect](https://stackoverflow.com/questions/53070970/infinite-loop-in-useeffect) – dcwither Dec 03 '20 at 02:41

1 Answers1

1

I provided the solution, and @JacobSmit explained in the comment section. Now I just organize them into an answer with more details, hope it'd be helpful to latecomer.

Solution

const useSearch = (query: string, sources: string[]) => {
  // ...
  useEffect(() => {
    // ...

    // FIX:
    // just apply the spread operator (...) to `sources`
    // to spread its elements into the dependency array of `useEffect`
  }, [query, ...sources]);

  return response;
};

Explanation

The useSearch custom hook passes [query, sources] to the dep array of useEffect, where as sources: string[] is an array itself. That makes the dep array of shape:

["query", ["source_1", "source_2", ..., "source_n"]]

See that the second element of dep array is a nested array. However, the way useEffect consumes the dep array, is to apply Object.is equality check to each of it's elements:

// pseudo code
function isDepArrayEqual(prevDepArray: any[], currDepArray: any[]) {
  return prevDepArray.every(
    (prevElement, index) => Object.is(prevElement, currDepArray[index])
  )
}

With each re-render, the hook call useSearch("Donald", ["bbc-news"]) creates a new instance of sources array. That'll fail the Object.is(prevSources, currSources) check, since equality of arrays is compared by their reference, not the value(s) they contain.

With the spread operator [query, ...sources], you transform the shape of dep array into:

["query", "source_1", "source_2", ..., "source_n"]

The key difference is not about copying, but unpacking the sources array.

Now that the nested sources array is unpacked, and each element of dep array is just string. A equality check on strings is compared by their value, not reference, thus useEffect will consider dep array unchanged. Bug fixed.

hackape
  • 18,643
  • 2
  • 29
  • 57