How to cancel requests with Apollo GraphQL

Author Mileta Dulovic
Author

Mileta Dulovic

Let's say you have a big list of items to pull from backend on the /list page. Well, you go to /list page, request is triggered, and you wait for the data. While you wait for the data, you click a link to go to the next page, but what happens with request on /list page? It is still active in the background. Why is this bad? Well, if you are expecting a big chunk of data, this can really slow down all the pages on the website.

useQuery

You would use useQuery to fetch data from the server, something like this.

import { useQuery } from "@apollo/graphql";

const { data } = useQuery(GET_BIG_DATA, {
  fetchPolicy: "network-only",
});

// do something with the data

How can we solve this

We can easily solve the issue by making a custom hook that we will use instead of default the useQuery. Let's call it useAbortiveQuery

useAbortiveQuery

Hook looks something like this.

import { useState, useEffect } from "react";
import { useApolloClient } from "@apollo/client";

function useAbortiveQuery({ query, params, deps }) {
  const [data, setData] = useState();
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  const apolloClient = useApolloClient();

  useEffect(() => {
    setLoading(true);
    const watchedQuery = apolloClient.watchQuery({
      query,
      ...params,
      fetchPolicy: params?.fetchPolicy || "cache-and-network",
    });

    const sub = watchedQuery.subscribe({
      next(x) {
        if (!x.partial) {
          setData(x.data);
          setError(null);
          setLoading(false);
        }
      },
      error(err) {
        setError(err);
        setLoading(false);
      },
      complete() {
        setLoading(false);
      },
    });

    return () => {
      sub.unsubscribe();
    };
  }, [deps]);

  return { data, error, loading };
}

export { useAbortiveQuery };

As you can see, I am using watchQuery instead of useQuery as it gives me the ability to subscribe/unsubscribe to the query.

Now, every time user leaves page from where this hook is triggered, API call will be canceled, and no more wasted bandwidth, or slow pages changes.

I also exposed, loading, and error variables for easier UI manipulating.

Here is an example of using a hook

const { data, loading, error } = useAbortiveQuery({
  query: GET_BIG_DATA,
  params: {
    fetchPolicy: "network-only",
    variables: {
      limit: 1000,
    },
  },
  deps: id,
});

As you can see, syntax is really similar as in useQuery with 1 aditional parameter, deps. I added deps as a way to refetch the query. Every time deps change, useEffect inside useAbortiveQuery will trigger, and run the query again. Think of deps as refetch in useQuery

Conclusion

Feel free to copy the hook and tweak it to your own needs.

Also note that this hook is replacement for useQuery, and not to useLazyQuery. I don't feel the need to wrap useLazyQuery as they should be triggered programatically, on actions, and in most cases should resolve quick, as they shouldn't ask for big chunks of data.