DEV Community

Juan Pablo Djeredjian
Juan Pablo Djeredjian

Posted on

Combining the power of React Query and GraphQL for data fetching and state management

On the first part of this series, we took a look at how, at my present company, we had the chance of starting a greenfield project, and could choose the libraries that we considered best for the jobs. We chose React Query to handle state management in our React app, and, because our backend team was delayed providing the API that was needed for the projects, we used a fake API to get the library working, and demo its functionality.

All was going according to plan until we had a meeting with the backend team, and Connor, one of the engineers, set us up for a surprise: "We have been thinking and discussing this for a while, and we consider that the best solution we can offer you is to build a GraphQL server you can use to query and mutate the data".

GraphQL? That was a first. Our company has many backend applications, some offering REST API services and other are message queues working with RabbitMQ, but GraphQL was definitely not under anyone's radar.

But as I started thinking about it, it slowly started having more and more sense. The frontend application we were building would need to display lots of data in many different shapes, with some pages showing tables with rows containing only a certain selection of properties of the data, other pages showing data summaries, and in most cases we would need to build advanced filtering functionality. GraphQL's ability to serve the exact data requested by the user would save us a huge effort of re-formatting it in the frontend, prevent us from over or under-fetching, and basically tailoring every request we made exactly to our needs.

It sounded good in theory... but we had already set up our up to use React Query as our data fetching library (and state management solution!), making requests to a REST endpoint. Would be need to throw everything away and start from scratch with something like Apollo?

It took only a small revisit to the React Query docs to realize that this wasn't the case. As we said on the first part of this series, React Query's fetching mechanisms are agnostically built on Promises, so it can be used with literally any asynchronous data fetching client, such as Axios, the native fetch and even GraphQL!

The library's docs recommended a mysterious tool for leveraging the combined power of React Query and GraphQL: GraphQL-Codegen. What was that? I had no idea at the moment, but it promised type safety, and code generation for "ready-to-use React Hooks, based on your GraphQL operations".

Digging a little deeper into the code generator's docs, we started to understand: "When we develop a GraphQL backend, there would be many instances where we would find ourselves writing the same things which are already described by the GraphQL schema [...] By analyzing the schema and parsing it, GraphQL Code Generator can output code at a wide variety of formats".

The best way to understand that is to take a look at an example of what GraphQL-Codegen does: it takes (reads!) our schema and produces -in our case- TypeScript types that we can use all across our applications, that we otherwise would have needed to write from scratch.

So, as the example in the docs show, provided we have the following GraphQL schema ir our app:

schema {
  query: Query
}

type Query {
  user(id: ID!): User!
}

type Mutation {
  updateUser(id: ID!, input: UpdateUserInput!): User
}

type User {
  id: ID
  name: String
  username: String
  email: String
}

input UpdateUserInput {
  name: String
  username: String
  email: String
}
Enter fullscreen mode Exit fullscreen mode

Then GraphQL-Codegen will produce the following TypeScript types:

export type Maybe<T> = T | null;

/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string,
  String: string,
  Boolean: boolean,
  Int: number,
  Float: number,
};

export type Author = {
  __typename?: 'Author',
  id: Scalars['Int'],
  firstName: Scalars['String'],
  lastName: Scalars['String'],
  posts?: Maybe<Array<Maybe<Post>>>,
};

export type AuthorPostsArgs = {
  findTitle?: Maybe<Scalars['String']>
};

export type Post = {
  __typename?: 'Post',
  id: Scalars['Int'],
  title: Scalars['String'],
  author: Author,
};

export type Query = {
  __typename?: 'Query',
  posts?: Maybe<Array<Maybe<Post>>>,
};
Enter fullscreen mode Exit fullscreen mode

OK! So far so good! But what exactly does this have to do with React Query?

To understand the real power of React Query + GraphQL + GraphQL-Codegen we need get our hands dirty.

Replacing our fake REST API with a fake GraphQL API

While the discussions continued with our backend of how their application would be structured, we decided to modify the proof of concept that we had already built (and saw in part 1 of this series) and re-write it to query and mutate data with GraphQL.

However, for that we had used the fake API service JSONPlaceholder. That would not help us anymore, as it provides a REST interface for fetching and updating mock resources. We needed a GraphQL API!

Enter GraphQLZero to the rescue: an online GraphQL API both powered by JSONPlaceholder and serving its same data, as well as providing the schemas! Exactly what we needed.

So taking as a starting point the demo we had built as seen in Part 1, we started by adding the GraphQL schema that we would feed the Code Generator, a simplified version of the schema provided by GraphQLZero. We thus created the schema.graphql file on inside a new /graphql directory:

# Example schema taken from https://graphqlzero.almansi.me/api and simplified
type Query {
  user(id: ID!): User!
}

type Mutation {
  updateUser(id: ID!, input: UpdateUserInput!): User
  deleteUser(id: ID!): Boolean
}

type User {
  id: ID
  name: String
  username: String
  email: String
}

input UpdateUserInput {
  name: String
  username: String
  email: String
}

input AddressInput {
  street: String
  suite: String
  city: String
  zipcode: String
}
Enter fullscreen mode Exit fullscreen mode

You can take a look at the detailed docs on what GraphQL schemas are and how to write them, but as you can see from our file, we defined the schema for a User with a set of properties, as well as the Query to retrieve one or many of them, and Mutations to update and delete them.

Creating our GraphQL documents

The next step was to define our GraphQL documents. There are actually four types: [querys, mutations, fragments and subscription](https://graphql.org/learn/queries/)s, but for our use case we needed only queries -to fetch the data- and mutations -to update data-, as we had declared in our schema.graphql.

For each query and mutation that we want to perform in our application, we need to define an individual document that our GraphQL-Codegen can later understand and transform into usable TypeScript/React code.

Our simplest case is the query for retrieving an individual user: it retrieves the id and name of a User when the id is passed as a parameter. We therefore created our user.graphql document file and placed it in the new /graphql/queries path:

query getUser($id: ID!) {
  user(id: $id) {
    id
    name
  }
}
Enter fullscreen mode Exit fullscreen mode

We needed also an additional query that retrieves multiple users, a UsersPage object type, with two sub-properties: firstly, a data object which consists of an array of Users, each of which will return the id and name properties; secondly, a meta object, which provides a totalCount property (total number of Users returned). We named this file users.graphql :

query getUsers($options: PageQueryOptions) {
  users(options: $options) {
    data {
      id
      name
    }
    meta {
      totalCount
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Example of mutation document

What about updating User ? In order to do that, we need to describe a mutation that updates a User's properties, by passing as parameters the ID of the user to update, as well as the properties to update in the shape of UpdateUserInput input type.

To keep our /graphql directory organized, we created a further subdirectory called /mutations and saved our updateUser.graphql file there:

mutation updateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(id: $id, input: $input) {
    id
    name
  }
}
Enter fullscreen mode Exit fullscreen mode

Everything seemed to be taking shape. We had now a new /graphql folder with the following tree:

├── graphql
│   ├── mutations
│   │   └── updateUser.graphql
│   ├── queries
│   │   ├── user.graphql
│   │   └── users.graphql
│   └── schema.graphql
Enter fullscreen mode Exit fullscreen mode

Using GraphQL-Codegen to generate the code for us

So far so good. But all we have up until now are just a number of GraphQL documents with not much utility per se. How do we actually use them to retrieve and modify our data?

This is where GraphQL-Codegen comes into place: a tool that works as the "glue" between React-Query and GraphQL. We will use it to not only automatically generate TypeScript types based on the schema we described above, but also -and this is where it really shines- to generate ready-to-use React Hooks based on each of the documents we just wrote!

So, no more writing hooks to fetch or modify data by hand, just define a GraphQL document, run the code generator, and you'll have a hook at your disposal that leverages all the power of React-Query.

Let's get started with GraphQL-Codegen. Small note before though: the tool works for a wide array of languages and libraries, not only TypeScript and GraphQL. This is just one of the things it can do, and we are using this combination because this is how our app is written and what our backend looks like. But take a look at the docs to see all the possibilities it offers!

Getting started with GraphQL-Codegen

To get started, we first need to install graphql as well as three dev dependencies from @grapql-codegen : the cli for running our commands; typescript-operations , a plugin that generates the TS types out of our GraphQL schema and operations, and finally typescript-react-query , which generates the React Query with TS typings for us:

yarn add graphql
yarn add --dev @graphql-codegen/cli @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-query
Enter fullscreen mode Exit fullscreen mode

As a following step, let's create a script in our package.json file, that we can run to get our code automatically generated using our newly installed CLI:

"scripts": {
    "generate": "graphql-codegen"
}
Enter fullscreen mode Exit fullscreen mode

And now we move forward to the most important step: configuring the codegen.yml file. This is the configuration file where we indicate GraphQL-Codgen what file it should create, where to generate it and point to which schemas and operations it should take into account. There is also a number of additional configuration options, some of which fit our use case.

Let's take a look at the finished file and then we can dive deeper into what it all means:

schema: "./graphql/schema.graphql"
documents: 
  - "./graphql/queries/**.graphql"
  - "./graphql/mutations/**.graphql"
generates:
  ./src/_generated.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-query
    config:
      defaultScalarType: unknown
      skipTypename: true
      fetcher:
        endpoint: "https://graphqlzero.almansi.me/api"
        fetchParams:
          headers:
            content-type: "application/json"
Enter fullscreen mode Exit fullscreen mode
  • schema: a path string to a local GraphQL schema file or a URL to a GraphQL schema provided externally. It should provide schemas for our data types as well as operations (Query and Mutation). This option also supports multiple schemas, that can be provided as an array of strings, and they will be merged. In our case, we point to our single schema.graphql file within our graphql directory.
  • documents: a path string that points to our GraphQL documents: query, mutation, subscription and fragment. Wildcards can be used to select all .graphql files under a directory: for our case, we will use an array to point to all *.graphql documents within our /graphql/queries and /graphql/mutations directories.
  • generates: a key-value map where the key represents an output path for the generated code and the value represents a set of options which are relevant for that specific file. We will generate our code directly within our /src folder.
    • generates.plugins: a required list of plugins that the code generator needs to auto-generate types and hooks based on our schema and documents. For our React-Query use case we need the plugins which we have previously installed:
      • typescript
      • typescript-operations
      • typescript-react-query
    • generates.config: a map used to pass additional configuration to the plugins. We are currently using:
      • generates.config.defaultScalarType: instructs the plugin to override the type that unknown scalars will have. Default value is any, but our config overrides it to unknown due to avoid having any types in our codebase.
      • generates.config.skipTypename: instructs the plugin not to add the __typename property to the generated types. Since we do not initially need to differentiate our objects types through their type, the default value is overriden to false.
      • generates.config.fetcher: customizes the fetcher function we wish to use in the generated file, and that will be responsible of making requests to our backend:
        • generates.config.fetcher.endpoint: since we will point to a unique endpoint exposed by our GraphQL server, we can configure it in this property. This prevents us from having to pass in the endpoint every time we use one of the generated React Hooks.
        • generates.config.fetcher.fetchParams: allows to set additional parameters to our fetcher function such as headers. We'll set the content-type header to application/json.

Notice that you can also configure codgen.yml to create multiple generated files with their own distinct schema, operations or config by structuring the file in an alternative way.

Let's go ahead and run our code generator by running:

yarn generate
Enter fullscreen mode Exit fullscreen mode

If we take a look at the _generated.ts file created within /src we can first see how our fetcher function was automatically generated, already pointed at our pre-defined endpoint:

function fetcher<TData, TVariables>(query: string, variables?: TVariables) {
  return async (): Promise<TData> => {
    const res = await fetch("https://graphqlzero.almansi.me/api", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ query, variables }),
    });

    const json = await res.json();

    if (json.errors) {
      const { message } = json.errors[0];

      throw new Error(message);
    }

    return json.data;
  }
}
Enter fullscreen mode Exit fullscreen mode

It's also interesting to see how the generator creates TypeScript types based on our schema. For example:

export type Maybe<T> = T | null;

export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};

export type Query = {
  user: User;
};

export type User = {
  email?: Maybe<Scalars['String']>;
  id?: Maybe<Scalars['ID']>;
  name?: Maybe<Scalars['String']>;
  username?: Maybe<Scalars['String']>;
};
Enter fullscreen mode Exit fullscreen mode

We'll later use these types along our codebase. But more interestingly, let's see how our tool generated ready-to-use React hooks (based on the React-Query hooks!) that completely handle data fetching and updating.

For example, let's take a look at the useGetUserQuery hook, that we can use to fetch a single user by passing an ID to it:

import { useQuery, UseQueryOptions } from 'react-query';

export type GetUserQuery = {
  user: {
    id?: string | null | undefined,
    name?: string | null | undefined 
  }
};

export type GetUserQueryVariables = Exact<{
  id: Scalars['ID'];
}>;

export const GetUserDocument = `
    query getUser($id: ID!) {
      user(id: $id) {
        id
        name
      }
    }
`;

export const useGetUserQuery = <
  TData = GetUserQuery,
  TError = unknown
>(
  variables: GetUserQueryVariables,
  options?: UseQueryOptions<GetUserQuery, TError, TData>
) =>
    useQuery<GetUserQuery, TError, TData>(
      ['getUser', variables],
      fetcher<GetUserQuery, GetUserQueryVariables>(GetUserDocument, variables),
      options
    );
Enter fullscreen mode Exit fullscreen mode

Notice how the generator first creates the types it needs based on the schema we provided, as well as on the query document. It then uses those types to create a hook that reutilizes React Query's useQuery and passes down the types as generics, the query parameters as variables, and the fetcher function we saw above, which is responsible for actually making the request.

Using our hooks to fetch data

We are now ready to leverage the combined power of React Query and GraphQL. For demonstration purposes, let's create a component that takes an id as input from the user of our app, calls the useGetUserQuery to fetch a User from our GraphQLZero API and display it on screen.

import React, { useState, ChangeEvent } from "react";
import { useGetUserQuery } from "./_generated";

export const UserDisplay = () => {

  const [userId, setUserId] = useState("1")
  const updateUserId = (event: ChangeEvent<HTMLInputElement>) => {
    setUserId(event.target.value);
  }

  const {
    isLoading,
    data,
    isError
  } = useGetUserQuery({id: userId})

  if (isError || !data) {
    return <span>Error. Please reload page.</span>;
  }

  const { user } = data;

  return (
    <section>
      <h3>Select a User ID between 1 and 10: </h3>
      <input type="number" min={1} max={10} value={userId} onChange={updateUserId}/>
      {isLoading ? 
        <p>Loading...</p>
      : (
        <div className="userRow">
          <h3>{user?.name}</h3>
          <p>User Id: {user?.id}</p>
        </div>
      )}
    </section>
  );
};
Enter fullscreen mode Exit fullscreen mode

Notice how we use useGetUserQuery in a way that is analogous to the use of the common useQuery hook provided by the React Query library. In this case, we just pass the userId state as the id so that every time that it updates, the hook is re-run, and a request is made to our GraphQL backend with it as a parameter! Pretty amazing stuff.

graphqlreact.gif

Wrapping it up

We have now seen how we can leverage the combined power of React Query and GraphQL to easily and flexibly handle data fetching and updating. By simply defining our GraphQL schemas and documents and taking advantage of the fantastic GraphQL-Codgen tool, handling our data needs becomes a breeze that really accelerates the development experience, and pushes our codebases to be more maintainable with reusable types and React hooks.

If you have an app that consumes a GraphQL endpoint, be sure to give these tools a try.

Check out the finished demo app and clone the repo to play around with the code.

Thanks for reading!

Oldest comments (0)