22
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
}
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>>>,
};
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.
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
}
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.
The next step was to define our GraphQL documents. There are actually four types: [query
s, mutation
s, fragment
s 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
}
}
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
}
}
}
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
}
}
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
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!
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
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"
}
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"
-
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 singleschema.graphql
file within ourgraphql
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 isany
, but our config overrides it tounknown
due to avoid havingany
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 tofalse
. -
generates.config.fetcher
: customizes thefetcher
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 ourfetcher
function such as headers. We'll set thecontent-type
header toapplication/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
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;
}
}
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']>;
};
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
);
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.
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>
);
};
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.
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!
22