How to useQuery on actions and side-effects

ยท

5 min read

Intro

In 2019, memorable moments included Snapchat's introduction of the baby face filter, the conclusion of Game of Thrones (before any further disappointments), and the emergence of React Query, also known as TanStack Query.

React Query, a potent data-fetching library for React, revolutionized our approach to API interactions and server state management.

When it comes to querying data, you often want to trigger the useQuery hook as a result of a specific user action, such as clicking a button. However, using hooks conditionally or within callbacks is not possible. The implementation varies based on factors like the need for query parameters, one-time versus recurring fetches, and action-specific parameters.

In this article, we'll explore all three scenarios and delve into diverse implementations using React Query.

Fetching Without Params

When you want to initiate a fetch without any parameters as a consequence of an action, this represents the simplest of the three use cases.

In this scenario, the useQuery hook provides us with a refetch function that serves our purpose seamlessly.

const WithRefetch = () => {
  const { data, dataUpdatedAt, refetch } = useQuery('my-key', () => request(), {
    refetchOnWindowFocus: false,
    // this prevents the query from firing automatically
    enabled: false,
  })

  console.log("with refetch ", { data, dataUpdatedAt });

  return (
    <div>
      <button onClick={() => refetch()}>Click me!</button>
      <br />
      {JSON.stringify(data)}
    </div>
  );
};

In this example, we are setting the enabled to false to prevent the query from being automatically called when the render of the component happens.

Then we can just simply call the refetch function on click (or any other event or side-effect) to fetch the data.

The main limitation with this approach is that the refetch function does not allow any params to be passed, so if you need to update the params for the query, this would not be the right approach for you.

In this example, we set the enabled flag to false to prevent the query from automatically triggering during component rendering.

Subsequently, we can invoke the refetch function upon a click event (or any other relevant event or side-effect) to fetch the data.

However, it's important to note that this approach has a limitation: the refetch function does not allow parameters. If you need to update query parameters, this method may not be the best method.

Fetch Once with Params

To introduce parameters into our request, a slight adjustment in our logic needs to happen. Additionally, creating a custom hook to separate the logic can make the process more straightforward:

const useUser = () => {
  const [variables, setVariables] = React.useState();
  const key = ["user", variables];
  const query = useQuery(key, () => request(variables), {
    refetchOnWindowFocus: false,
    enabled: Boolean(variables)
  });

  return {
    ...query,
    fetchUser: setVariables,
  };
};

Now, let's break down how this custom hook works. The key difference here lies in dynamically configuring the enabled property based on our parameters. The request won't be triggered when there are no parameters (initial state).

Furthermore, we return the fetchUser function, essentially working as the state setter to update the enabled property in response to new parameters, which are consistently supplied to the request: request(variables).

Next, let's explore how to use this hook:

const SingleFetchWithParams = () => {
  const { data, dataUpdatedAt, fetchUser } = useUser();

  // This will log the timestamp for when the query most recent successful query
  console.log("single fetch with params", { data, dataUpdatedAt });

  return (
    <>
      <button onClick={() => fetchUser({ id: 2 })}>Click me!</button>
      <br />
      {data && JSON.stringify(data)}
    </>
  );
};

In the UI we can call the fetchUser function returned from the hook with the desired parameters. This will always trigger a new fetch request as long as the params change.

Fetch on Action with Params

If your use case needs triggering a fetch to consistently get the most up-to-date data from the server, regardless of whether you require parameters or if they have changed, then we must use a different technique, leveraging the queryClient:

const WithRefetch = () => {
  const client = useQueryClient();
  const id = 1;

 // We still need to call the query hook to access the data
  const { data, dataUpdatedAt } = useQuery('my-key', () => request(), {
    refetchOnWindowFocus: false,
    enabled: false,
  });

  const onClick = () => client.fetchQuery('my-key', () => request({ id })); 

  console.log("single fetch with params", { data, dataUpdatedAt });

  return (
    <div>
      <button onClick={onClick}>Click me!</button>
      <br />
      {JSON.stringify(data)}
    </div>
  );
};

We can access the QueryClient instance by calling the useQueryClient hook. Then we can call the fetchQuery method to trigger the fetch request when required, with the necessary params. This method ensures that a request is consistently triggered every time, guaranteeing access to the most current data.

We still need to use the useQuery hook, simply because it allows us to access the data returned by the fetch.

This approach was recommended by Tanner Linsley the creator of the library, so we know for a fact we are not breaking any good practices.

One Custom Hook to Rule Them All

We can go to the next level by creating a helper hook that handles all the different use cases that we just covered and simplifies the code implementation:

const useUser = () => {
  const client = useQueryClient();
  const [variables, setVariables] = React.useState();
  const key = ["user", variables];
  const query = useQuery(key, () => request(variables), {
    refetchOnWindowFocus: false,
    enabled: Boolean(variables)
  });

  return {
    ...query,
    // single fetch approach
    fetchUser: setVariables,
    // fetch always
    fetchTodoWithParams: ({ id }) => client.fetchQuery(key, () => request({ id }))
  };
};

Conclusion

We tackled three key use cases:

  1. Fetching Without Parameters: We learned to initiate a fetch on user actions using the refetch function from the useQuery hook, even though it lacks parameter support.

  2. Fetching Once with Params: To fetch data with parameters, we created a custom hook that dynamically enabled/disabled queries based on parameters, allowing flexibility in fetching data with different criteria.

  3. Fetching on Action with Params: For consistent server updates regardless of parameter changes, we utilized the queryClient and fetchQuery method.

You can find all the code and a working demo in codesanbox.

Hope you enjoyed learning about how to use useQuery to fetch data on side-effects and actions ๐Ÿ™‚, until the next post ๐Ÿ‘‹๐Ÿป.

ย