Skip to content

Instantly share code, notes, and snippets.

@mlg87
Created July 7, 2020 20:26
Show Gist options
  • Save mlg87/d8f993702c51396dbd2fc455ac9e7078 to your computer and use it in GitHub Desktop.
Save mlg87/d8f993702c51396dbd2fc455ac9e7078 to your computer and use it in GitHub Desktop.

Using Apollo's local cache for global state management with Typescript, codegen and remote resolvers

If your project is using the apollo-client to communicate with your GraphQL backend, there is no need to include redux or mobx for the global state management; you can store this state in the same Apollo client you are already using. This will be a contrived example, but this post will cover:

  • Setting up local queries and mutations to take advantage of the cache
  • Integrating the apollo-client you are already using with the local graphql
  • Setting up a GraphQL schema locally to use for typeDefs and codegen
  • Using @graphql-codegen to create custom hooks for your queries and mutations

Why did I want to do this?

The main reason that I wanted to implement this in our project was to kill off redux. Aside from generally disliking redux, I wanted it out of our app to reduce the complexity around managing global state in our UI that was mainly driven by data retrieved from our backend. All of this data was already living in our apollo-client's cache, and we were having to then get it into our redux store. This required lots of files, lots of type and interface creation and was just generally a pain. By moving all of this into our apollo-client we would be able to develop more quickly in a strongly typed spec that we already had set up for our remote GraphQL server.

Setting up local queries, mutations and schema

We'll start by creating a schema.js file. The reason that I chose to make this a javascript file in a completely typescript project was to make code generation easier (avoid the need to compile a .ts file). The gql tag created in this file will be added to our apollo-client's typeDefs and used to generate the local schema later on.

At this point it is also worth noting that our project is set up in a fairly opinionated way (I know, shocker), so you will need to tweak any scripts to match your structure. Our structure assumes that files are organized by domain, so for the rest of the post we'll mainly be operating in the Balloon domain, or src/Balloon.

So let's get started setting up what we need to retrieve balloons from our server and then store the selected one locally.

// src/Balloon/schema.js

const gql = require("graphql-tag");

module.exports = gql`
  input BalloonInput {
    color: String!
    id: ID!
  }

  type Balloon {
    color: String!
    id: ID!
  }

  extend type Query {
    selectedBalloon: Balloon!
  }

  extend type Mutation {
    # this returns null, but graphql requires defining a type here
    setSelectedBalloon(selectedBalloon: BalloonInput!): String
  }
`;

Next we'll set up the queries.ts and mutations.ts files.

// src/Balloon/queries.ts

import gql from "graphql-tag";

//
// ─── GQL TAGS ───────────────────────────────────────────────────────────────────
//

export const GetSelectedBalloonLocal = gql`
  query GetSelectedBalloonLocal {
    selectedBalloon @client {
      color
      id
    }
  }
`;

export const GetBalloons = gql`
  query GetBalloons {
    balloons {
      color
      id
    }
  }
`;

The magic in the above file is that we have defined both a local and remote query. By decorating the GetSelectedBalloonLocal query with @client we are telling apollo-client that it should only look in the local cache for this. We will use the GetBalloons query to populate our cache with data from our server, but we will only keep the concept of a selected balloon client-side in our cache.

// src/Balloon/mutations.ts

import { Resolver } from "apollo-client";
import gql from "graphql-tag";

//
// ─── GQL TAGS ───────────────────────────────────────────────────────────────────
//

export const SetSelectedBalloonLocal = gql`
  mutation SetSelectedBalloonLocal($selectedBalloon: BalloonInput!) {
    setSelectedBalloon(selectedBalloon: $selectedBalloon) @client
  }
`;

//
// ─── RESOLVERS ──────────────────────────────────────────────────────────────────
//

export const setSelectedBalloonMutationResolver: Resolver = (
  _root,
  { selectedBalloon },
  { cache }
) => {
  try {
    cache.writeData({ data: { selectedBalloon } });
  } catch (error) {
    console.error("Error writing selectedBalloon to local cache: ", error);
  }
  return null;
};

The gql tags in the file above should be familiar, but we've also added something you may not have set up before with client-side GraphQL. The query for selectedBalloon is simple enough that we don't need to create a resolver for it (though we could), but we do need to let the apollo-client know what to do when the setSelectedBalloon mutation is called, and that is done with the setSelectedBalloonMutationResolver. If you have set up resolvers for graphql on the server before, this should look pretty similar.

Lastly we'll set up some defaults so the cache doesn't explode (i.e. an empty state).

// src/Balloon/defaults.ts

import { IBalloon } from "../types/graphql"; // <-- this file will be auto-generated very soon

const defaultSelectedBalloon: IBalloon = {
  color: "",
  id: "",
};

export default {
  selectedBalloon: { ...defaultSelectedBalloon, __typename: "Balloon" },
};

Integrating with your existing apollo-client

Most of the heavy lifting is done elsewhere, so we only have a few modifications to the src/client.ts file (I'm going to omit setting up links and any other things that you may want to do with your client).

// src/client.ts

import { InMemoryCache } from 'apollo-cache-inmemory';

import balloonDefaultCacheValues from './Balloon/defaults';
import { setSelectedBalloonMutationResolver } from './Balloon/mutations';
import balloonSchema from './Balloon

const cache = new InMemoryCache();

const client = new ApolloClient({
  ...
  cache,
  resolvers: {
    Mutation: {
      setSelectedBalloon: setSelectedBalloonMutationResolver
    }
  },
  typeDefs: [balloonSchema]
});

// init cache
cache.writeData({
  data: {
    ...balloonDefaultCacheValues
  }
});

At this point you could use the local query and mutation for balloons in your app, but there would be a lot of typing involved. We want auto-generated query and mutation hooks and we want our data typed correctly without defining types all over the place, and we want it now.

Create a local GraphqQL schema and generate types and hooks

Create two files at the root level of the project: localSchema.js and codegen.yml. The localSchema.js will be used to get all of the schema.js files you have throughout your app and combine them into a schema to then be combined with your remote schema.

// ./localSchema.js

const { loadFilesSync } = require("@graphql-tools/load-files");
const { mergeTypeDefs } = require("@graphql-tools/merge");

const loadedFiles = loadFilesSync(`${__dirname}/src/**/schema.js`);
module.exports.schema = mergeTypeDefs(loadedFiles);

// ./codegen.yml

overwrite: true
schema:
  - localSchema.js
  - ${SCHEMA_PATH}/graphql: # <-- this is for our remote schema, you'll probably have something similar
      headers:
        Authorization: ${AUTH0_ACCESS_TOKEN}
generates:
  src/types/graphql.tsx:
    documents: # <-- your app structure may be different, but this will pick up the queries.ts and mutations.ts files created earlier
      - src/**/*queries.{js,ts}
      - src/**/*mutations.{js,ts}
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withHOC: false
      withHooks: true # <-- say yes to the hooks
      hooksImportFrom: "@apollo/react-hooks"
      typesPrefix: I # <-- all of our types created will be prefixed with 'I'
      skipTypename: true
      namingConvention:
        enumValues: upper-case#upperCase
      scalars:
        ID: string
    hooks:
      afterOneFileWrite:
        - yarn lint:script --fix
        - prettier --write src/**/*.{js,jsx,ts,tsx}
  ./schema.graphql:
    plugins:
      - schema-ast

Make sure you have the following dev dependencies installed:

  • @graphql-codegen/cli
  • @graphql-codegen/introspection
  • @graphql-codegen/schema-ast
  • @graphql-codegen/typescript
  • @graphql-codegen/typescript-operations
  • @graphql-codegen/typescript-react-apollo
  • @graphql-tools/load-files
  • @graphql-tools/merge

In your package.json, add a script for code generation: // ./package.json

{
  ...
  "scripts": {
    ...
    "generate:schema": "graphql-codegen -r dotenv/config"
  }
}

Now, when you run $ yarn generate:schema from the command line all of your local and remote queries and mutations will have hooks generated for them as well as data types and inputs. All of this will now be accessible from the ./types/graphql.tsx file. So now go forth and use your new hooks:

// ./src/AnotherThing/index.tsx

import React from "react";

import { BalloonPickerThatHasAllOfTheNeededLogicInItButForSomeReasonRequiresMeToPassAMutation } from "../Balloon/Picker";
import {
  useGetSelectedBalloonLocalQuery,
  useGetBalloonsQuery,
  useSetSelectedBalloonLocalMutation,
} from "../types/graphql";

const AnotherThing: React.FC = () => {
  useGetBalloonsQuery();
  const { data: localData } = useGetSelectedBalloonLocalQuery();
  const [setSelectedBalloon] = useSetSelectedBalloonLocalMutation();

  return (
    <>
      <span>
        You've selected the {localData?.selectedBalloon?.color} balloon!
      </span>
      <BalloonPickerThatHasAllOfTheNeededLogicInItButForSomeReasonRequiresMeToPassAMutation
        onClick={setSelectedBalloon}
      />
    </>
  );
};

export default AnotherThing;

So there you have it. I hope you're able to adapt this adapt this for your own project to reduce some of the complexity around global state management.

Article tags: react, typescript, graphql, apollo-client, codegen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment