Following this discussion on Twitter https://twitter.com/mattia_asti/status/1645517258926575616 I found out that the current pattern we are using for react-query custom hooks is bad.
// BAD
function useAnimalsBad() {
const { data, isFetching, refetch } = useQuery<Animal[]>(
["animals-bad"],
() => getAnimals()
);
// exporting this object means that everytime a property change,
// the entire object reference is different
return {
animals: data,
isFetchingAnimals: isFetching,
refetchAnimals: refetch
};
}
let countA = 0;
function AppWithBadQuery() {
// we are not destructuring isFetchingAnimals but if the underlying property changes
// this object will change and re-render the component
const { animals, refetchAnimals } = useAnimalsBad();
countA += 1;
return (
<div>
<h1>Component 1 (bad query)</h1>
<h2>Renders: {countA}</h2>
<button onClick={() => refetchAnimals()}>refetch</button>
<pre>{animals}</pre>
</div>
);
}
In the custom query hook we are exporting an object with all the properties that we want.
This creates a situation where the entire object reference changes, no matter what properties we are destructuring in the components usage.
In this example we are not destructuring the isFetchingAnimals
property but it's part of that object and when it changes, the object reference changes and React re-renders the component.
https://twitter.com/TkDodo/status/1645513270508355585?s=20
property tracking works by tracking what gets used / destructed from useQuery. So yes, all destructed props are seen as used.
I created a simple codesandbox to reproduce the issue here: https://codesandbox.io/s/react-query-testing2-6mfwj1?file=/src/App.tsx
Viceversa, this is the correct implementation that doesn't trigger re-renders
because we are not destructuring many properties, and then pick just a few,
but instead we are just picking what properties we want inside the component with animalsQuery.data
// GOOD
function useAnimalsGood() {
// we are returning what react-query useQuery returns, so properties are automatically tracked
return useQuery(["animals-good"], () => getAnimals());
}
function AppWithGoodQuery() {
const animalsQuery = useAnimalsGood();
countB += 1;
return (
<div>
<h1>Component 2 (good query)</h1>
<h2>Renders: {countB}</h2>
<button onClick={() => animalsQuery.refetch()}>refetch</button>
<pre>{animalsQuery.data}</pre>
</div>
);
}
The lead maintainer of react-query is also suggesting that this allows us to keep the entire context and use some properties when needed, for example we rarely use isFetching
but we have to export it from some queries.
With this pattern, we just us animalsQuery.isFetching
when needed, since we have the entire context available.