Getting started with React Query and GraphQL

In this article, we’ll look at a few strategies to implement GraphQL with React Query. In a previous article, we talked about what React Query is, and how to get started with it. This is the follow-up to that post about how to work with GraphQL specifically.

Previously, we left our basic repository fetching GraphQL with the window fetch library. Which works! However, this simple approach doesn't let us unlock the full potential of GraphQL. In this guide, we'll set up our project to take full advantage of asynchronous GraphQL with subscriptions, and auto-generate our hooks using GraphQL codegen with the React Query plugin.

Updating our base example

In the previous example, we simply fetched some content from Hasura and showed the results. Well, that’s not the most engaging experience. Especially since our model takes advantage of an event-driven paradigm where we can upload HEX colors, and will get a complementary color scheme automatically returned, which means that we have some asynchronous behavior that would benefit from at least mutations (uploading our colors) and subscriptions (updating with our returned colors). If you followed along with the previous example, our new application now has an input field, which lets us provide these colors.

We’ve added a couple of additional utilities to help check for color lightness which will let us toggle light or dark text depending on the color we are overlaying on.

Invalidating Cache vs Subscription

React Query gives us a number of methodologies for updating our content. We can query our colors, which stores the result in the cache. When we mutate (update/add/remove) a color, we can use a success callback to clear that cache and update with the results we got back from Hasura. The other option is to create a subscription if our query has asynchronous content which will allow us to update the results when they become available. We'll provide examples of both approaches.

Invalidating Cache

We'll add a mutation with the following code, still using fetch. In order to benefit from our existing query, we are going to update our global cache with the response of our mutation. React Query gives us a couple of methods to update our cache. We can either call a cache invalidation call after we successfully update from the server, or we can update the cache in a success handler of our initial mutation. We'll opt for the later.

//Dependency
import { useMutation, useQueryClient } from "react-query";

// Client Reference
const queryClient = useQueryClient();

// Mutation Hook
const { isLoading, isError, isSuccess, data, mutate } = useMutation(
    (hex) =>
      fetch("https://intent-shad-91.hasura.app/v1/graphql", {
        method: "POST",
        body: JSON.stringify({
          query: `
        mutation InsertColorOne( $hex: String ){
          insert_color_one(object: {color: $hex}) {
            color
            complementary_colors {
               color
            }
          }
        }
      `,
          variables: {
            hex,
          },
        }),
      })
        .then((res) => res.json())
        .then((res) => res.data),
    {
      onSuccess: ({insert_color_one}) => {
        queryClient.setQueryData(["data"], ({color}) => {
          return { color: [...color, insert_color_one] };
        });
      },
    }
  );

Creating Subscriptions

React Query doesn't support subscriptions natively, which are a crucial part of what makes GraphQL so powerful! However, React Query does provide all the tooling we need to work with subscriptions using whichever fetching mechanism we would want, in this case, we'll write a simple WebSocket wrapper to fetch our content. A big shout out to this user on Github for bulk of the reference code!

To refactor for subscriptions, we'll create a useColorSubscription hook. This will implement graphql-ws and on successful message, we'll update React Query's cache. For UI design, I've exported a state manager to let us know when the subscription is attempting to make the initial query, and after we've received the data.

import { useEffect, useState } from "react";
import { useQueryClient } from "react-query";

const url = "ws://intent-shad-91.hasura.app/v1/graphql";

export const useColorSubscription = () => {
  const queryClient = useQueryClient();
  const [isSubscribing, setIsSubscribing] = useState(false);
  const [isSubscribingSuccess, setIsSubscribingSuccess] = useState(false);

  useEffect(() => {
    const ws = new WebSocket(url, "graphql-ws");
    setIsSubscribing(true);

    ws.onopen = () => {
      ws.send(JSON.stringify({ type: "connection_init", payload: {} }));
      ws.send(
        JSON.stringify({
          id: "1",
          type: "start",
          payload: {
            // variables: {},
            extensions: {},
            operationName: "GetColors",
            query: `subscription GetColors {
                color {
                  color
                  complementary_colors {
                    color
                  }
                }
              }`,
          },
        })
      );
    };

    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);

      if (msg.type == "data") {
        setIsSubscribingSuccess(true);
        setIsSubscribing(false);
        const data = msg.payload.data.color;
        queryClient.setQueriesData("colors", data);
      }
    };

    return () => {
      ws.send(JSON.stringify({ id: "1", type: "stop" }));
      ws.close();
    };
  }, []);
  return { isSubscribing, isSubscribingSuccess };
};

With this in place, we'll refactor our main application code that loops over this cache. Specifically, we'll be initializing the cache to an empty array, and preventing the useQuery call to refetch as our subscription will handle all our updating logic.

  const { data } = useQuery("colors", () => [], {
    refetchOnMount: false,
    refetchOnWindowFocus: false,
    refetchOnReconnect: false,
    refetchIntervalInBackground: false,
  });

  const { isSubscribing, isSubscribingSuccess } = useColorSubscription();

Now we can loop over the content in our application.

{isSubscribing && <p className="text-2xl text-gray-200">Loading...</p>}
        {isSubscribingSuccess &&
          data.map((item, key) => {
            return ({/* My Code */})
          }
        </div>
    );
})}

We can refactor our mutation now as well to not handle any cache updates on its own.

const { isLoading, isError, isSuccess, data, mutate } = useMutation((hex) =>
    fetch("https://intent-shad-91.hasura.app/v1/graphql", {
      method: "POST",
      body: JSON.stringify({
        query: `
        mutation InsertColorOne( $hex: String ){
          insert_color_one(object: {color: $hex}) {
            color
            complementary_colors {
               color
            }
          }
        }
      `,
        variables: {
          hex,
        },
      }),
    })
      .then((res) => res.json())
      .then((res) => res.data)
  );

Switching to subscriptions comes with some performance tradeoffs, as well as additional code to maintain, but since this example includes asynchronous code (our complementary color scheme) the trade off is warranted, and React Query gives us easy access to all the internal methods we'd need to work with the cache in a logical manner.

One primary pieces is failing us at the moment, and that is that we are still writing fetch calls by hand. This works for a couple of one-off queries, but there's a better way! Let's look at incorporating a GraphQL library to help make this cleaner.

Implementing GraphQL Codegen

We'll follow the onboarding guide as outlined by the codegen installation guide.

The first step will be to extract our mutation out of our Input component and into a separate file. I'm putting mine in a subdirectory called queries/

mutation InsertColorOne($hex: String) {
  insert_color_one(object: { color: $hex }) {
    color
    complementary_colors {
      color
    }
  }
}

From there we can invoke the CLI from codegen by calling yarn graphql-codegen init - be sure to have installed the dependencies first!

// Dependencies for generator script
yarn add graphql
yarn add -D @graphql-codegen/cli

// Init Script
yarn graphql-codegen init

For supporting React Query, we'll need to add an extra plugin. You can follow the React Query plugin steps here, but ultimately we'll need to install yarn add -D @graphql-codegen/typescript-react-query and then install the remaining dependencies added from the onboarding script with yarn.

In the end, your codegen.yml file should look similar to the following:

overwrite: true
schema: "https://intent-shad-91.hasura.app/v1/graphql"
documents: "./src/queries/**/**.gql"
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-query"
    config:
      fetcher:
        endpoint: "https://intent-shad-91.hasura.app/v1/graphql"
  ./graphql.schema.json:
    plugins:
      - "introspection"

After running our codegen, we are able to replace all of our fetch code with a simple h0ok, reducing 21 lines of code in just this one example!

// Imports
import { useInsertColorOneMutation } from "./generated/graphql";

// Instantiate
const { mutate, isLoading, isSuccess } = useInsertColorOneMutation();

// Use
() => {mutate({ hex })}

Obviously this project is a bit contrived. But it's pretty to look at and it imparts some helpful patterns, made simple with React Query. Even in the most complicated scenarios such as supporting real-time content with generated GraphQL SDKs, React Query has a robust set of tooling to make it simple to implement by removing massive amounts of boilerplate.

If you'd like to have a look yourself, you can find the complete updated example at this Github repo.

Blog
11 Nov, 2021
Email
Subscribe to stay up-to-date on all things Hasura. One newsletter, once a month.
Loading...
v3-pattern
Accelerate development and data access with radically reduced complexity.