简体   繁体   中英

React custom hook rendering infinitely

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:

在此处输入图像描述

What am I doing wrong?

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM