Using Apollo React Hooks with Typescript - Dgraph Blog

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 hooks so that you can leverage Typescript to the maximum. Also, this blog demonstrates how you can create types for custom wrappers around these react hooks.

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 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 Slash GraphQL to get started in one step to create your GraphQL backend. You don’t need to write any resolvers to use Slash GraphQL, 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`
  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 !

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 methods 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 Slash GraphQL - create your GraphQL backend in one step.

Slash GraphQL 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.


This is a companion discussion topic for the original entry at https://dgraph.io/blog/post/apollo-react-hooks-with-typescript/
1 Like

Thank you for the great article!

What were your reasons to choose Apollo CLI instead of https://graphql-code-generator.com ?

3 Likes

@Urigo Welcome to Dgraph Discuss!! :slight_smile: Happy to see you here!

1 Like

@Urigo Hey - thanks for reaching out. Honestly, it’s just a personal preference.

The Slash GraphQL project we use Apollo CLI so that we are as close as possible with the apollo ecosystem. But looking at graphql-code-generator popularity & adoption it looks like the default tool to go with. I think @michaelcompton has written a post about using graphql-code-generator to generate types.

I am thinking to include steps to use graphql-code-generator. Maybe if you are interested you can pitch in? Any resources or suggestions that can be helpful for React developers. :grinning:

Thank you for the warm welcome!
I just love Dgraph and everything you are doing!

The reason I mentioned graphql-code-generator was that Apollo CLI’s codegen part is not actively maintained anymore - check out this issue as a reference (and migration plan): Codegen collaboration with Apollo Codegen and GraphQL Code Generator · Issue #2053 · apollographql/apollo-tooling · GitHub

Also a lot of other benefits for React developers are being described on that issue (TypedDocumentNode, Autogenerated Hooks, Performance and others…)

I would love to help with anything you want, including migrating your internal use :slight_smile:

I think a very good resource is the issue linked above and also the official website and the following blog posts

Feel free to reach out directly to me for anything!

3 Likes

Interesting!
I have a question - the auto-generated apollo hooks take care of updating the cache as well?

For example - I have a query that gets a list of todos & a mutation that creates one. So every mutation I want to update my query’s cache as well. Can this be automated as a part of the code generator?

I started to use this graphql-codegen a bit now. Here is what I have found.

The graphql-codegen can be tied in with Apollo Client and with the proper configuration, it will generate custom hooks for queries and mutations using your operation files.

Being that is uses Apollo Client for these custom hooks under the hood, it all works seamlessly. The important facts to remember:

  1. Any data that you mutate and want to updated in the cache, be sure to return it in the payload of the mutation.
  2. By default Apollo Client is configured to only use id or _id to uniquely identify the cache to update. So if you want to the payload to update the cache, include the ids’s as well.
  3. (2B reallly) If your type uses a field for ID or @id other than default above, then you must configure your cache to use these custom identifiers.
  4. Apollo Client does not know the difference between update and delete operations. I wrote a function to Use the Payload to Delete from Apollo Cache that you can pass to the update prop which will delete any items by unique identifier from the payload when attached. Unfortunately, there is no way that I have found to include this script in the codegen hooks, so you have to manually include it any time you call a useDelete* hook.

This exact use case is not automatic because in essence the generated hooks are all separate entities other than sharing some types. So in the generated code, the useAddToDo hook would not know to update the cache of the useQueryToDo hook. And even if it did, that may not always be what was wanted. You can pass variables into the hooks in different places so you may only want one use of the hook to be updated and not every other use.

1 Like