Skip to content

Instantly share code, notes, and snippets.

@mtt87
Last active April 10, 2023 20:36
Show Gist options
  • Save mtt87/32e9765a4613118e34eff57263eefdab to your computer and use it in GitHub Desktop.
Save mtt87/32e9765a4613118e34eff57263eefdab to your computer and use it in GitHub Desktop.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment