Using Apollo React Hooks with Typescript

Typescript with GraphQL

GraphQL’s type system allows developing error-prone flexible APIs and concrete data-fetching, and you’d want the same experience with the language you’re using with GraphQL. Typescript can enable this if you have the types in place. These types can be used with Apollo hooks and cast types of the result based on the GraphQL query. This way you can self explore the GraphQL type without moving away from the IDE. Whenever the schema changes, you can catch regressions quickly!

This post helps you to jump start with typing your Apollo react hooks so that you can leverage Typescript to the maximum. Also, this blog demonstrates how you can create types for custom wrappers around Apollo react hooks & consume your GraphQL API in your react app.

Apollo ships with its own type system, for example, while using useQuery, the type inferred for data is QueryResult<any, Record<string, any>>. Obviously, you can do much better than the default - any. Now, all we need to do is create types for all our GraphQL queries, mutations, types, enums, variables & inputs used in your app. Sounds simple & quick? Yes - with auto-generation using Apollo CLI ⚡️

Get started with Apollo CLI

Check out Apollo CLI. It has a variety of useful tools, but for this blog, you’ll only need two.

Example folder structure -

gql
├── users.gql.ts
├── billing.gql.ts
└── hooks
    └──users
    └──billing

Example GraphQL query

// users.gql.ts
import { gql } from "@apollo/client";
export const GET_USERS = gql`
    query GetUsers {
        users {
            uid
            name
        }
    }
`;

Common mistakes while generating types -

  1. Your GraphQL server should be up, if you are building types from your local machine, make sure it’s live.
  2. No anonymous queries, you will have to provide a name to any query that is in *.gql.ts file Commands to run
npm install -g apollo
apollo schema:download --endpoint=http://localhost:8071/graphql graphql-schema.json
apollo codegen:generate --localSchemaFile=graphql-schema.json --target=typescript --includes=src/gql/**.gql.ts --tagName=gql --addTypename  types

(We will cover the options later)

Replace the endpoint with your GraphQL endpoint. If you don’t have one, check out Dgraph Cloud to get started in one step to create your GraphQL backend. You don’t need to write any resolvers to use Dgraph Cloud, all you need is a GraphQL schema.

After running the script, you’ll see something like this -

gql
├── types # A new folder created with all the auto-generated code
├── users.gql.ts
├── billing.gql.ts
└── hooks
    └──users
    └──billing

After generating the types, you can start plugging them into the hooks. You can add graphql-schema.json to .gitignore or remove it after generation

Example Usage (with React hook)

Without Types

const { data, loading, error } = useQuery(GET_USERS);
// GET_USERS is the query
// data is inferred as any, since you did not provide any type

With Types

const { data, loading, error } = useQuery<GetUsers, GetUsersVariables>(GET_USERS);

// Note - `GetUsers` & `GetUsersVariables` are auto generated types
// Now `data` infers the type of `GetUsers`

Decoding the options in apollo-tools -

  1. localSchemaFile - File where you have stored your GraphQL schema (JSON). This file is being downloaded from the GraphQL server.
  2. target - Type of code generator to use.
  3. includes - Should be used to find queries
  4. tagName - Name of the template literal tag used to identify template literals containing GraphQL queries in Javascript/Typescript code
  5. addTypename - Apollo tools say to pass this to make sure the generated Typescript and Flow types have the __typename field as well

Now you can generate these types directly from the queries/mutations. Since React hooks by Apollo are fully typed, Typescript generics can be used to type both incoming operation variables and GraphQL result data. No more any !

You can achieve the same using GraphQL Code Generator similary. You can get started here

Next: Wrappers

It’s always good to have wrappers built on top of any third-party libraries. It decouples your application code with the library, the wrapper becomes the interface with which your application code interacts, hence you can always migrate to any other third party library without changing the application code.

You get to create a loosely coupled system that is much easier to test and reuse. This applies for Apollo client as well. When you are in Typescript, you end up importing a lot of react hooks (like useQuery, useMutation) and types making the application tightly coupled with Apollo. That’s why it’s best to create a minimum API surface area.

In the GraphQL context, you may sometimes want to do some transformations/aggregations over the result. You also may want to change the shape of the data to fit the view. A wrapper makes it simple to manage and trim your code. You don’t want to end up calling a bunch of functions on top of the result of the Apollo hooks. Instead, treat your hooks as data channels. You just plug into the component and start using it.

What does a simple wrapper look like?

Let’s imagine that you want to trim the text in the title of a book. You want your React hook to perform this operation and return the data under the property name books. A function will look like this -

export const useBooks = () => {
   const { loading, data, error } = useQuery<GetBooks>(GET_BOOKS);
   return { books: data.books.map(trimTitle)};
};

This does help to some extent. Typescript will be able to infer the return type. But there are a few problems. You need other values that useQuery of Apollo returns like loading, errors, etc.

Using Extends with Typescript

To solve the above problem, you can extend the type of useQuery. That will look like this

interface useBooksResult extends QueryResult {
books: GetBooks__Books
}
export const useBooks = (): useBooksResult => {
const { data , ...rest} = useQuery<GetBooks>(GET_BOOKS);
return { ...rest, books: data.books.map(trimTitle)};
};

The above type ensures that the developer user returns all the fields that useQuery returns, else Typescript starts complaining. This way you ensure that there is no inconsistency between custom hooks return types.

Using Generics with Typescript

Still, there is some room for improvement. For each custom hook, you will have to define types and there is no singular way to achieve it. Ideally you would like to have a type for the complete function which can be used across the application as the base hook for any Apollo hook wrapper. You can achieve this using Typescript generics.

For example -

import { MutationResult, QueryResult } from "@apollo/client";

export type CustomQueryHook<T, R extends QueryResult> = (input: T) => R;
export const useBooks: CustomQueryHook<void, UseBooksResult> = () => {
   const { loading, data, error } = useQuery<GetBooks>(GET_BOOKS);
   return { books: data.books.map(trimTitle)};
};

This gives more uniformity to the custom hooks. For example CustomQueryHook is an expression that accepts an input of type T and returns type R; where T & R can be supplied as inputs provided R should extend QueryResult. This type becomes a base type for all custom hooks in your application. Similarly you can create a custom type for mutation hook.

Summary

It’s always good to automate the grunt work - writing types and maintaining them can be time-consuming. Having the right tools in place can make your developer workflow more effective. The type system can help you catch bugs if/when your schema changes. Let Typescript do the heavy lifting for you. Don’t live with any type hanging around everywhere. Leverage Apollo tools to build a seamless type system with GraphQL.

Type Wrappers can help to make the system more flexible and loosely coupled. You can create generic types to govern the wrappers around Apollo, ensuring that your application code is not tightly bound to Apollo. You can compose with other React hooks, aggregate data, transform, do a lookup, etc, allows you to build different patterns that solves your problem.

GraphQL doesn’t have to be hard. If you want to get quickly started with GraphQL, checkout Dgraph Cloud - create your GraphQL backend in one step.

Dgraph Cloud auto-generates resolvers and endpoints based on your GraphQL schema, so you can focus on developing your apps and iterate quickly, without downtime. It’s the fastest way to get started with GraphQL.