34
loading...
This website collects cookies to deliver better user experience
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
}
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>>>,
};
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
}
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.[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
.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
}
}
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
}
}
}
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./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
}
}
/graphql
folder with the following tree:├── graphql
│ ├── mutations
│ │ └── updateUser.graphql
│ ├── queries
│ │ ├── user.graphql
│ │ └── users.graphql
│ └── schema.graphql
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
package.json
file, that we can run to get our code automatically generated using our newly installed CLI:"scripts": {
"generate": "graphql-codegen"
}
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.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 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
.codgen.yml
to create multiple generated files with their own distinct schema, operations or config by structuring the file in an alternative way.yarn generate
_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;
}
}
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']>;
};
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
);
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.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>
);
};
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.