Recently, we’ve developed a GraphQL API in our NestJS series. NestJS under the hood uses Apollo Server. Although it is compatible with any GraphQL client, the Apollo platform also includes the Apollo Client.
If you want to know more about GraphQL and how to develop a backend with it, check out API with NestJS #27. Introduction to GraphQL. Queries, mutations, and authentication
This article aims to be an introduction to the Apollo Client. It gives an overview of its features while providing examples with TypeScript.
The most fundamental function of the Apollo Client is making requests to our GraphQL API. It is crucial to understand that it has quite a lot of features built on top of it.
Why you might not need it
An important thing about the Apollo Client is that it is more than just a tool for requesting data. At its core, it is a state management library. It fetches information and takes care of caching, handling errors, and establishing WebSocket connections with GraphQL subscriptions.
If you are adding GraphQL to an existing project, there is a good chance that you are already using Redux, Mobx, or React Context. Those libraries are commonly used to manage the fetched data. If you want to keep them as your single source of truth, you would not want to use Apollo Client.
Consider using a library like graphql-request if the only thing you need is to call your GraphQL API and put the response in your state management library.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { request, gql } from 'graphql-request' const query = gql` query { posts { id title paragraphs } } ` request(process.env.REACT_APP_GRAPHQL_API_URL, query) .then((data) => { console.log(data) }) |
Introducing the Apollo Client with TypeScript
Above, you can see me using a process.env.REACT_APP_GRAPHQL_API_URL variable. In this article, we use Create React App with TypeScript. It creates a react-app-env.d.ts file for us. Let’s use it to define our variable.
We need to remember that weh Create React App, our environment variables need to have the REACT_APP_ prefix.
react-app-env.d.ts
1 2 3 4 5 |
namespace NodeJS { interface ProcessEnv { REACT_APP_GRAPHQL_API_URL: string; } } |
Now, we need to add our variable into the .env file.
1 |
REACT_APP_GRAPHQL_API_URL=http://localhost:3000/graphql |
Validating your environment variables with a library such as envalid seems like a good idea also.
Setting up the Apollo Client
The first step in setting up the Apollo Client is installing the necessary libraries.
1 |
npm install @apollo/client graphql |
Fortunately, they already contain the TypeScript declaration. We can see that on the NPM page:
The second step is creating an instance of the ApolloClient. This is where we need our environment variable.
apolloClient.tsx
1 2 3 4 5 6 |
import { ApolloClient, InMemoryCache } from '@apollo/client'; export const apolloClient = new ApolloClient({ uri: process.env.REACT_APP_GRAPHQL_API_URL, cache: new InMemoryCache() }); |
There are slight differences in the TypeScript syntax between the .ts and .tsx. Therefore, I tend to stick to the .tsx file extension to keep my code consistent.
One of the features of the Apollo Client is caching. Above, we are initializing the InMemoryCache(). It keeps the cache in memory that disappears when we refresh the page. Even though that’s the case, we can persist it using Web Storage by using the apollo3-cache-persist library.
The last part of setting up the Apollo Client is connecting it to React. To do so, we need the ApolloProvider that works similarly to Context.Provider built into React.
app.tsx
1 2 3 4 5 6 7 8 9 |
import React from 'react'; import { ApolloProvider } from '@apollo/client'; import { apolloClient } from "./apolloClient"; export const App = () => ( <ApolloProvider client={apolloClient}> { /* ... */ } </ApolloProvider> ) |
Performing queries
Once all of the above is ready, we can start using our GraphQL API. The most basic functionality of the Apollo Client is querying data.
In our NestJS course, we create an API for managing posts. Let’s now create a component that can display a list of them. Since we are using TypeScript, the first step would be creating a Post interface.
post.tsx
1 2 3 4 5 |
export interface Post { id: number; title: string; paragraphs: string[]; } |
To perform a query, we first need to pass it into the gql tagged template. Its job is to parse the query string into a query document. Then, we need to pass it to the useQuery hook.
If you want to know more about tagged templates in general, check out Concatenating strings with template literals. Tagged templates
app.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { gql, useQuery } from '@apollo/client'; import { Post } from './post'; interface PostsQueryResponse { posts: Post[]; } const GET_POSTS = gql` query { posts { id title paragraphs } } `; export function usePostsQuery() { return useQuery<PostsQueryResponse>(GET_POSTS); } |
Since there are quite many things going on both with the query and the TypeScrip definitions, I suggest creating a separate custom hook for that. It would also make it quite easy to mock for testing purposes.
I tend to create quite a lot of custom hooks to keep the code clean and readable. If you want to know more about it, check out JavaScript design patterns #3. The Facade pattern and applying it to React Hooks
The useQuery hook returns quite a few things. The most essential of them are:
- data – contains the result of the query (might be undefined),
- loading – indicates whether the request is currently pending,
- error – contains errors that happened when performing the query.
PostsList/index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import React from 'react'; import { usePostsQuery } from './usePostsQuery'; export const PostsList = () => { const { data, error, loading } = usePostsQuery(); if (loading) { return ( <div> Loading... </div> ) } if (error) { return ( <div> An error occurred </div> ) } return ( <div> { data?.posts.map(post => ( <div key={post.id}> <h2>{post.title}</h2> { post.paragraphs.map((paragraph) => ( <p key={paragraph}>{paragraph}</p> )) } </div> )) } </div> ) } |
The cache mechanism
A significant thing to notice is that the usePostsQuery() hook is called every time the PostsList renders. Fortunately, the Apollo Client caches the results locally.
The 29th part of the NestJS course mentions polling. It is an approach that involves executing a query periodically at a specified interval.
1 2 3 4 5 6 7 8 |
export function usePostsQuery() { return useQuery<PostsQueryResponse>( GET_POSTS, { pollInterval: 5000 } ); } |
We specify the pollInterval in milliseconds.
Another approach to refreshing the cache is refetching. Instead of using a fixed interval, we can refresh the cache by calling a function.
PostsList/index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import React from 'react'; import { usePostsQuery } from './usePostsQuery'; export const PostsList = () => { const { data, error, loading, refetch } = usePostsQuery(); // ... return ( <div> <button onClick={() => refetch()}>Refresh</button> { data?.posts.map(post => ( <div key={post.id}> <h2>{post.title}</h2> { post.paragraphs.map((paragraph) => ( <p key={paragraph}>{paragraph}</p> )) } </div> )) } </div> ) } |
Mutating data
The second core functionality in GraphQL after querying data is performing mutations. Let’s create a custom hook for creating posts.
useCreatePostMutation.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import { gql, useMutation } from '@apollo/client'; import { Post } from '../PostsList/post'; interface CreatePostResponse { createPost: Post[]; } interface CreatePostVariables { input: { title: string; paragraphs: string[]; } } const CREATE_POST = gql` mutation createPost($input: CreatePostInput!) { createPost(input: $input) { id, title, paragraphs } } `; export function usePostMutation() { return useMutation<CreatePostResponse, CreatePostVariables>(CREATE_POST); } |
The useMutation hook also returns an array with two elements
- the mutate function that triggers the mutation; it returns a promise that resolves to the mutation result,
- the result of the mutation with properties such as data, loading, and error.
With that knowledge, let’s create a simple form that allows us to perform the above mutation.
useCreatePostFormData.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import { ChangeEvent, FormEvent, useCallback, useState } from 'react'; import { useCreatePostMutation } from './useCreatePostMutation'; export function useCreatePostFormData() { const [createPost] = useCreatePostMutation(); const [title, setTitle] = useState(''); const [firstParagraph, setFirstParagraph] = useState(''); const handleTitleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => { setTitle(event.target.value); }, []); const handleFirstParagraphChange = useCallback((event: ChangeEvent<HTMLInputElement>) => { setFirstParagraph(event.target.value); }, []); const handleSubmit = useCallback(async (event: FormEvent) => { event.preventDefault(); if (title && firstParagraph) { try { await createPost({ variables: { input: { title, paragraphs: [firstParagraph] } } }) } catch (error) { console.error(error); } } }, [title, firstParagraph, createPost]); return { title, firstParagraph, handleFirstParagraphChange, handleTitleChange, handleSubmit } } |
Above, we use the useCreatePostMutation hook. As we’ve noted before, it returns an array where the first element is a function that triggers the mutation.
All that’s left is to use the useCreatePostFormData hook with a form.
PostForm/index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import React from 'react'; import { useCreatePostFormData } from './useCreatePostFormData'; export const PostForm = () => { const { title, firstParagraph, handleTitleChange, handleFirstParagraphChange, handleSubmit, } = useCreatePostFormData(); return ( <form onSubmit={handleSubmit}> <input value={title} onChange={handleTitleChange} /> <input value={firstParagraph} onChange={handleFirstParagraphChange} /> <button type='submit'>Send</button> </form> ) } |
Authenticating with cookies
In our NestJS course, we authenticate by sending the Set-Cookie header with a cookie and the httpOnly flag. This means that the browser needs to attach the cookies because JavaScript can’t access them.
If you are interested in the implementation details of the above authentication, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
To achieve this, we might need to modify our apolloClient slightly.
Apollo Client has support for communicating with GraphQL servers using HTTP. By default, it sends the cookies only if the API is in the same origin. Fortunately, we can easily customize it:
1 2 3 4 5 6 7 |
import { ApolloClient, InMemoryCache } from '@apollo/client'; export const apolloClient = new ApolloClient({ uri: process.env.REACT_APP_GRAPHQL_API_URL, credentials: 'include', cache: new InMemoryCache(), }); |
With the include option, Apollo Client sends the cookies even for cross-origin calls.
If you want to read more about the credentials parameter, check out the MDN documentation.
Updating the cache after the mutation
The last piece missing is updating the list after the successful mutation. One of the ways to do so is to pass the update parameter when calling the useMutation hook.
With it, we can directly access both the cache and the mutation result. By calling readQuery we get the cache’s current state, and with writeQuery, we overwrite it.
useCreatePostMutation.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import { gql, useMutation } from '@apollo/client'; import { Post } from '../PostsList/post'; import {GET_POSTS, PostsQueryResponse} from "../PostsList/usePostsQuery"; // ... export function useCreatePostMutation() { return useMutation<CreatePostResponse, CreatePostVariables>(CREATE_POST, { update: (cache, { data }) => { const currentPostsList: PostsQueryResponse = cache.readQuery({ query: GET_POSTS }) ?? { posts: [] }; const result = data?.createPost; if (result) { cache.writeQuery({ query: GET_POSTS, data: { posts: [ ...currentPostsList.posts, result ] } }); } } }); } |
Another good way of keeping our application up to date is through subscriptions. It deserves a separate article, though.
Summary
Today, we’ve looked into the Apollo Client and learned its basics. We’ve also considered if we need it in the first place because it might not be the best approach for some use-cases.
Looking into the fundamentals of the Apollo Client included both the queries and mutations. We’ve also touched on the subject of cookie-based authentication. There is still quite a lot to cover, so stay tuned!
Nice article. Thanks a lot.
Hello! Thanks for this great articel!
Where can i find the separate “subscriptions” articel?
Hello. Thanks a lot!
So far, I’ve published an article on how to work with subscriptions on the backend side. Maybe this will be of some help to you.
https://dev.wanago.io/2021/02/15/api-nestjs-real-time-graphql-subscriptions/