Reputation: 885
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?
Upvotes: 0
Views: 138
Reputation: 19957
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.
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;
};
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.
Upvotes: 1