How to auto-generate static typed GraphQL Queries and Mutations in a React project

GraphQL has a lot to offer and it can make a Front-End developer's life way simpler. In addition to being the best way to fetch data (in my opinion), it is a statically typed schema that we can not only use to auto-generate types but we can also use it to auto-generate code.

I'm a huge fan of using GraphQL and TypeScript, in almost any React project that needs to interact with a GraphQL Schema. The amount of time we can save on development and the great developer experience it offers makes it a no brainer (at least for me).

I've been using Apollo Client on React projects for some time. In the old days (before hooks) you could use HoC or the Query and Mutation components to interact with the GraphQL schema. This was a very declarative and simple way of getting started, but it wasn't scalable, and the static typing wasn't great at all. In the end, these weren't the best way to consume a GraphQL Schema in a React project from any point of view. Right now they are deprecated and you can only use Hooks which is way more simple, scalable, and easy to use as you will see.

The Manual Way

The Apollo Client packs a lot of features but for this time we are going to focus only on the query and mutation hooks. Check out their getting started page if you want to learn how to set up a client on your project.

Queries

To perform any query we need to use the useQuery hook. Its pretty simple, you need to pass your GraphQL Document and you can also pass some configuration values like variables for your query.

Let's say for example we want to query the User type, here are some steps we need to follow:

  1. Create our GraphQL Document

  2. Call the useQuery hook with our Document and config params

  3. Try to remember the values that are available for the query result and variables (we haven't introduced TS yet)

Let's take a look at this code example:

import React from 'react';
import {gql} from "apollo-boost";
import {useQuery} from "@apollo/react-hooks";

// GraphQL Document
const UserQuery = gql`
    query ($id: ID!){
      User(id: $id) {
        id
        avatar
        fullName
      }
    }
`;

const User = () => {
	// Call the useQuery Hook with our Document and config
  const {data} = useQuery(UserQuery, { variables: { id: 1 } })
  if (!data || !data.User) return null;
  return (
    <section>
      <h1>User</h1>
      <div>
        <div>
          <img src={data.User.avatar} alt={`${data.User.fullName} avatar`} />
          <p>{data.User.fullName}</p>
        </div>
      </div>
    </section>
  );
};

export default User;

For a very simple Query, this looks OK, but we would need to repeat the same process any time we would like to use this Query. In the long run, this is not ideal. Probably some people would argue that we could use Fragments or create our own hooks, and probably that would help a lot. However, what I really dislike about this is that you would need to remember the query response for queries with nested types ending in a big waste of time looking at the schema repetitively.

Mutations

We can call our mutations in the same simple way as our queries. We need to use the useMutation hook, pass the GraphQL document, and the variables and that's it. Then we can use the mutate function to execute our mutation and the data object which represents the current status of the mutation.

Let's say for example we want to delete a User, we need to follow these steps:

  1. Create our GraphQL Document

  2. Create an instance of the useMutation hook with our document

  3. Execute the mutation (at some point)

  4. Try to remember the values that are available for the mutation result and variables

Let's take a look at this code example:

import React, {useEffect} from 'react';
import {gql} from "apollo-boost";
import {useMutation} from "@apollo/react-hooks";

// GraphQL Document
const deleteUserMutation = gql`
  mutation deleteUser($id: ID!) {
    deleteUser(id: $id) {
      id
    }
  }
`;

const DeleteUser = () => {
	// Create an instance of the useMutation hook with our document
  const [deleteUser, { data }] = useMutation(deleteUserMutation);

  const onDeleteUserClick = () => (
		// Execute the mutation
    deleteUser({ variables: { id: 1 } })
  );

  useEffect(() => {
    // Do something when the deleteUser data changes
  }, [data])

  return (
    <div>
      <button onClick={onDeleteUserClick}>
        Delete User
      </button>
    </div>
  );
};

export default DeleteUser;

While this is simple and very straight forward, it has the same issues: reusability and missing types.

Adding TypeScript

The Apollo Client supports TypeScript extremely well. Both query and mutation hooks can be typed relatively easy and you could even define the types of your variables.

Queries

We can add types (sort of) to the previous query example with just three additional steps:

  1. Create a new Interface for the Query Data

  2. Create a new Interface for the Query Variables

  3. Use those interfaces in the useQuery Hook

Let's take a look at the newly typed example:

import React from 'react';
import {gql} from "apollo-boost";
import {useQuery} from "@apollo/react-hooks";
import styled from 'styled-components';

// ------- New πŸ’‘ -------
// GraphQL Query Data Interface
interface IUserQueryData {
  User: {
    avatar: string;
    fullName: string;
  };
}

// ------- New πŸ’‘ -------
// GraphQL Query Variables Interface
interface IUserQueryVariables {
  id: string | number;
}

// GraphQL Document
const UserQuery = gql`
    query ($id: ID!){
      User(id: $id) {
        id
        avatar
        fullName
      }
    }
`;

const User = () => {
	// ------- New πŸ’‘ -------
  const {data} = useQuery<IUserQueryData, IUserQueryVariables>(UserQuery, { variables: { id: 1 } })
  if (!data || !data.User) return null;
  return (
    <section>
      <h1>User</h1>
      <div>
        <div>
          <img src={data.User.avatar} alt={`${data.User.fullName} avatar`} />
          <p>{data.User.fullName}</p>
        </div>
      </div>
    </section>
  );
};

export default User;

With a couple of new lines, I was able to have a static typed useQuery hook. While this works it's not that maintainable. For any change that you would make to your query, you would need to update your interfaces accordingly. Plus human error could lead to a bad development experience and some overhead to your process.

Mutations

We need to do the same additional steps to add types for our useMutation hook:

import React, {useEffect} from 'react';
import {gql} from "apollo-boost";
import {useMutation} from "@apollo/react-hooks";

// ------- New πŸ’‘ -------
// GraphQL Query Data Interface
interface IUserMutationData {
  User: {
    id: string;
  };
}

// ------- New πŸ’‘ -------
// GraphQL Query Variables Interface
interface IUserMutationVariables {
  id: string | number;
}

// GraphQL Document
const deleteUserMutation = gql`
  mutation deleteUser($id: ID!) {
    deleteUser(id: $id) {
      id
    }
  }
`;

const DeleteUser = () => {
	// ------- New πŸ’‘ -------
	// Create an instance of the useMutation hook with our document and types
  const [deleteUser, { data }] = useMutation<IUserMutationData, IUserMutationVariables>(deleteUserMutation);

  const onDeleteUserClick = () => (
    deleteUser({ variables: { id: 1 } })
  );

  useEffect(() => {
    // Do something when the deleteUser data changes
  }, [data])

  return (
    <div>
      <button onClick={onDeleteUserClick}>
        Delete User
      </button>
    </div>
  );
};

export default DeleteUser;

Once we have a query and a mutation, it's clear that there are some issues if we try to do this manually.

GraphQL Code Generator

I identified two common issues while using Apollo Client + React + TypeScript. While hooks are very simple and fast to use, you're still leaving a lot on the table, with GraphQL you can auto-generate types and code and if you're not taking advantage of that you should consider doing it.

There's a great Open Source project called GraphQL Code Generator. This is a CLI tool that can automate both your type and code generation. You just need to create the GraphQL Documents somewhere in your codebase and this package would do the rest.

Installing

You need to install graphql (you probably already have this one) and @graphql-codegen/cli:

# Install GraphQL
yarn add graphql

#Install the GraphQL Codegen CLI tool
yarn add -D @graphql-codegen/cli

You could use yarn or npm according to your preferences, but in order to avoid GraphQL conflicts don't install these packages globally.

Configuration

GraphQL Code Generator comes with a configuration wizard that helps you create an initial configuration file, after installing the package you need to run this command:

yarn graphql-codegen init

This would ask you some questions about your project and what features you want to use. You don't need to run this on every project because you can write down your configuration in a codegen.yml or codegen.json. Check out the complete configuration reference.

I used a codegen.yml:

overwrite: true
generates:
  src/generated/graphql.tsx:
    schema: "<http://localhost:5000/graphql>"
    documents: "src/graphql/**/*.ts"
    plugins:
      - add: // tslint:disable
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
    config:
      withHooks: true

I put all my GraphQL Documents inside the src/graphql/ directory in that way we can tell the codegen tool to only check for documents in that folder.

Plugins

As you can see GraphQL Code Generator uses plugins, you can add just the ones you're going to use which is great. For our purposes, you can install the following:

yarn add -D @graphql-codegen/add @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

Checkout the complete plugins list.

Running

You can call the tool directly but to make it easier we can add a new script on the package.json of the project:

{
  "scripts": {
		"generate-graphql-code": "graphql-codegen --config codegen.yml"
  }
}

Then we can call it from the terminal as follows:

yarn generate-graphql-code

Auto Generating The Code

Folder Structure

I want to use GraphQL Code Generator to simplify the workflow. It wouldn't be ideal to have a single file with all the queries and mutation so I opted to create a new folder for each GraphQL type I would be using on the project. Inside each folder, we have two files, queries.ts, and mutations.ts with their corresponding GraphQL operations. I notice that with this folder structure it is extremely easy to know where to edit or add new operations.

If we apply this to the User Type it would look like this:

.
β”œβ”€β”€ src
	β”œβ”€β”€ components
	β”œβ”€β”€ graphql //This is where our GraphQL Documents will live
		β”œβ”€β”€ User
			── queries.ts
			── mutations.ts
	β”œβ”€β”€ generated
		── graphql.tsx // This is the code generator output
β”œβ”€β”€ codegen.yml // This is the code generator config file
──tsconfig.json
──package.json

Queries

This time we need to create the GraphQL document in the queries.ts file inside ./src/graphql/User. We need to name our query and export it so the code generator can create the types and hook for us.

import { gql } from 'apollo-boost';

export const userQuery = gql`
  query User($id: ID!) {
    User(id:$id) {
      id
      fullName
      avatar
    }
  }
`;

After we have added our query, we need to run the code generator and we are ready to use our hook. By default, the hooks will be created with this naming convention: use[query name]Query for our example the name of the hook would be useUserQuery, we can simply import it and we're done also don't forget this hook is completely typed πŸ˜‰ .

import React from 'react';
// Import our autogenerated hook
import { useUsersQuery } from '../generated/graphql';

const Users:React.FC = () => {
	// Use our autogenerated hook
  const { data } = useUserQuery();
  if (!data || !data.User) return null;
  return (
    <section>
      <h1>User</h1>
      <div>
        <div>
          <img src={data.User.avatar} alt={`${data.User.fullName} avatar`} />
          <p>{data.User.fullName}</p>
        </div>
      </div>
    </section>
  );
};

export default User

Mutations

For mutation we also have to first create the GraphQL document, export it and don't forget to name it. The autogenerated mutation hooks use this naming convention: use[mutation name]Mutation

import { gql } from 'apollo-boost';

export const deleteUserQuery = gql`
  mutation deleteUser($id: ID!) {
    deleteUser(id: $id) {
      id
    }
  }
`;

After we generate our code, we can import and use our fully typed mutation hook:

import React, {useEffect} from 'react';
// Import our autogenerated hook
import {useDeleteUserMutation} from '../generated/graphql';

const DeleteUser = () => {
	// Use our autogenerated hook
  const [deleteUser, { data }] = useDeleteUserMutation;

  const onDeleteUserClick = () => (
    deleteUser({ variables: { id: 1 } })
  );

  useEffect(() => {
    // Do something when the deleteUser data changes
  }, [data])

  return (
    <div>
      <button onClick={onDeleteUserClick}>
        Delete User
      </button>
    </div>
  );
};

export default DeleteUser;

Wrapping up

Implementing GraphQL and TypeScript in a React project could be a very long task or something pretty simple. From my point of view, it doesn't matter if you are only planning to use a couple of queries and mutations or a huge amount of them it's not worth it to create your GraphQL Documents and Types by hand when you can automate the whole process and focus only on what really matters.

Useful Links

https://graphql-code-generator.com/

https://www.apollographql.com/docs/react/