Mileta Dulovic
HomeBlogReact

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 fire 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.

More in this category

Infinite scroll component in React and NextJS

Infinite scroll is a great way to load huge lists as it provides a great UX for user when compared to traditional pagination

Lazy loading images with Intersection Observer in React

Images can really be a bottleneck on the website. If you do not optimize your images users will have bad time and crawlers will not crawl you as fast as you want.

React: Pitch & anti-pitch

Weighing the Advantages and Disadvantages of React for Modern Web Development.