GraphQL Queries and Mutations with useSWR

5 Sept 2022

GraphQL doesn't require any special fetching library or mechanism. It can work using fetch, as we've seen before.

One of the main reasons we opt to use a specific GraphQL client is because how they handle caching data to reduce network requests. Most often they will "cache-and-network" by default. This means that before a GraphQL query is executed, it will check if it belongs in the cache and return it.

This also works with mutations. If you can update the cache based on the response of a GraphQL request then you can avoid making another request to fetch the updated data. Simply update the cache, and continue.

Obviously if you want to get the most up to date data from some an application that has many users then you may want to refetch at intervals.

Today we'll look at using SWR to fetch, and mutate a basic cache. It's not going to be as complex as the dedicated GraphQL clients, but you can get quite far by managing some state yourself.

SWR automatically handles fetching data and most importantly, making sure it's up to date.

First we'll install both swr, graphql, and graphql-request. The graphql-request library handles parsing JSON, errors, and more.

npm install swr graphql-request graphql

Using the graphql-request library we'll create a new GraphQLClient instance, and export it:

import { GraphQLClient } from "graphql-request";

export const client = new GraphQLClient("https://api.cartql.com");

Now inside of our application we'll use the <SWRConfig /> component to wrap our application at the highest level. In this example we'll use the Next.js _app.tsx convention to do this.

Inside _app.tsx you'll want to import SWRConfig, and the client we just created:

import { SWRConfig } from "swr";

import { client } from "../lib/client";

Now let's create fetcher function that passes whatever query and variables passed onto client.request:

const fetcher = (query, variables) => client.request(query, variables);

Finally wrap your application with SWRConfig and pass it fetcher to the value prop:

// ...
function MyApp({ Component, pageProps }: AppProps) {
  return (
    <SWRConfig
      value={{
        fetcher,
      }}
    >
      <Component {...pageProps} />
    </SWRConfig>
  );
}

Now we have our application wrapped with SWR, we can invoke useSWR anywhere inside our app.

We'll begin by updating our <Header /> component to fetch our cart total. Make sure to export your query, and preferably use a GraphQL fragment:

export const CartFragment = /* GraphQL */ `
  fragment Cart on Cart {
    id
    totalItems
    items {
      id
      name
      quantity
      images
      lineTotal {
        formatted
      }
    }
    subTotal {
      formatted
    }
  }
`;

export const GetCartById = /* GraphQL */ `
  query GetCartById($id: ID!) {
    cart(id: $id) {
      ...Cart
    }
  }
  ${CartFragment}
`;

Then inside of our <Header /> we can import the GetCartById query, and useSWR. Since we're using a variable id for the cart in this query, I'll also import that static variable from a file:

import useSWR from "swr";

import { GetCartById } from "../lib/graphql";
import { cartId } from "../lib/constants";

Next we can invoke useSWR:

const { data, error } = useSWR([
  GetCartById,
  {
    id: cartId,
  },
]);

Because our GraphQL query uses variables, we can pass an array to the first argument of useSWR. This means we can store multiple carts in our SWR cache with different IDs, if we needed to.

We can also declare the loading status by checkingt the values of data and error:

const loading = !error && !data;

Now inside our application we can show our cart total based on the response of our GraphQL query:

{
  loading ? "" : <>Total: {data?.cart?.subTotal?.formatted}</>;
}

If we now refresh our page and inspect the Network tab of our Developer Tools we can see the query is successfully sent to our API, and our cart total is shown.

Now we've handled querying data with useSWR... We can now mutate data too! To do this we will:

  1. Send a query to our API
  2. Update the SWR cache manually with the response
  3. Prevent SWR from automatically refetching

Now inside our product page where we add items to the cart we can invoke the method mutate. First, let's import it!

import { mutate } from "swr";

Then right inside the function addItem we can invoke mutate:

const addItem = async () => {
  mutate();
};

The first argument of mutate is our query, and variables we have already stored. From this we get the current data in the 2nd argument, but for the purposes of this, we won't be using that and instead reuse our GraphQL fragment from before.

Let's add our mutation to add items to the cart:

export const AddToCart = /* GraphQL */ `
  mutation AddToCart($input: AddToCartInput!) {
    addItem(input: $input) {
      ...Cart
    }
  }
  ${CartFragment}
`;

Now that we're returning the full cart state with CartFragment we can get this response and update our cart object in the cache. We'll pass everything our mutation needs to be successful in adding a new item to the cart:

const addItem = async () => {
  mutate([GetCartById, { id: cartId }], async () => {
    const { addItem } = await client.request(AddToCart, {
      input: {
        cartId,
        id,
        name,
        price,
        images: [image],
      },
    });

    return {
      cart: addItem,
    };
  });
};

Now finally we'll add false as the 3rg argument to prevent SWR from refetching our data:

const addItem = async () => {
  mutate(
    [GetCartById, { id: cartId }],
    async () => {
      const { addItem } = await client.request(AddToCart, {
        input: {
          cartId,
          id,
          name,
          price,
          images: [image],
        },
      });

      return {
        cart: addItem,
      };
    },
    false // Just added!
  );
};

If we now inspect the Network tab in our Developer Tools you'll notice we send a request with our mutation but not to refetch the data. If we remove the 3rd argument false and repeat, you'll see we do!

Since we know the new state we can simply mutate the SWR cache.

Finally we'll head to our cart page where we can invoke the same query we did in our header but this time use it to display a list of products in our cart.

import useSWR from "swr";

import { GetCartById } from "../lib/graphql";
import { cartId } from "../lib/constants";

const { data, error } = useSWR([
  GetCartById,
  {
    id: cartId,
  },
]);

Now if we inspect we'll notice there's one query made but 2 instances of useSWR with the same query. This is because SWR already has the value so it can return from the cache.

Let's finish our application by adding the ability to remove items using mutate. Let's first import mutate, and client:

import useSWR, { mutate } from "swr";

// ...

import { client } from "../lib/client";

Then just like we did before we can now create mutation, and execute it:

export const RemoveFromCart = /* GraphQL */ `
  mutation RemoveFromCart($input: RemoveCartItemInput!) {
    removeItem(input: $input) {
      ...Cart
    }
  }
  ${CartFragment}
`;

This time we need to pass a variable to our client instance that belongs to each product in our cart:

const removeItem = async (id) => {
  mutate(
    [GetCartById, { id: cartId }],
    async () => {
      const { removeItem } = await client.request(RemoveFromCart, {
        input: {
          cartId,
          id,
        },
      });

      return {
        cart: removeItem,
      };
    },
    false
  );
};

Now once again if you check the Network tab in our Developer Tools we will not be making any additional requests to refetch the current cart state but update it using the response from our mutation. Magic!

That's all there really is to getting started with SWR + GraphQL. There is a lot more you can do with the SWR library but these foundations should give you a quickstart to exploring building your own thin GraphQL client.