Now that we’ve got a solid grasp on how resolvers work, we can get into integrating a web application frontend. For this tutorial, we’ll be using React, and Apollo’s new hooks library. For those unfamiliar with hooks, these are a new way to build out logic inside React components. To get more up to speed on hooks, take a look at the React docs on them. This library is in beta at the time of writing, but very stable at this point and it is unlikely the features are using will change. I wanted to make sure that we are writing React code that’s as future-proof as possible, and hooks are here to stay. It will also make our code more concise for easier understanding.
Follow this GraphQL Series:
- 1. The Missing GraphQL Introduction
- 2. Resolvers: An in-depth look
- 3. Client Side Integration with Apollo Hooks
- 4. GraphQL Subscriptions with Apollo Server and Client
Table of contents
Setup
Since we are adding a separate app into our repo, for those of you following along using the resolver-end
branch on the course github. First we will make a new folder in the root of the repo called server
and move every file into it, except for the .gitignore
. Now, we can continue with creating the web app.
For those just joining in, please clone this repo and git checkout 3-client-integration-start
.
Let’s get started by creating a new react app in the root folder:
npx create-react-app web
You should now have server
and web
inside the cloned repository. Now we can go inside the web
folder, and install the required dependencies for Apollo Client:
rm -rf node_modules
npm install apollo-client apollo-link-http apollo-cache-inmemory graphql graphql-tag @apollo/react-hooks
npm install
Since we started with npm on the server, this will keep it the same inside create-react-app, so that its easy to continue following along.
Next, let’s delete every file inside of web/src
except for index.js
and App.js
. We’ll replace the index.js
with the following:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
Next up, let’s create a file called apolloSetup.js
:
import { HttpLink } from 'apollo-link-http';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
const httpLink = new HttpLink({
uri: 'http://localhost:4000',
});
export default new ApolloClient({
cache: new InMemoryCache(),
link: httpLink,
});
Apollo tries to support a lot of use cases, so they have split out the setup into a few different packages. We will be adding a couple more later on, but for now this is a pretty simple setup.
We create a link that tells Apollo Client where our server is located. Then, when we create the client. We pass it the httpLink
and an in memory cache. Apollo supports other caching options which can be useful for offline apps. With this setup, a page refresh will clear our cache, but clicking back and forth between pages would be able to use the cache, instead of making calls to our server for data.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
With our src/apolloSetup.js
in place, we can now update our App.js
:
import React from 'react';
import apolloClient from './apolloSetup';
import { ApolloProvider } from '@apollo/react-hooks';
import Books from './pages/Books';
const App = () => (
<ApolloProvider client={apolloClient}>
<Books />
</ApolloProvider>
);
export default App;
This wraps our app with Apollo, so that we can start using it. For this tutorial, we will make a few components and put them inside the Provider. In a more realistic React app, we would probably place some sort of router, like @reach/router inside of our provider.
Querying our Server
With our setup out of the way, let’s make our first query. Make a file called web/src/pages/useBooksQuery.js
:
import gql from 'graphql-tag';
import { useQuery } from '@apollo/react-hooks';
export const query = gql`
query Books {
books {
title
author
}
}
`;
export default () => useQuery(query);
Now we can make Books.js
inside of the same pages folder:
import React from 'react';
import useBooksQuery from './useBooksQuery';
const Books = () => {
let { data } = useBooksQuery();
if (!data || !data.books) return null;
return data.books.map(book => (
<div>
<h3>{book.title}</h3>
<p>{book.author}</p>
</div>
));
};
export default Books;
At this point, your web/src
directory should look like this:
├── App.js
├── apolloSetup.js
├── index.js
└── pages
├── Books.js
└── useBooksQuery.js
The Power of Apollo
Let’s open up the server
directory and run npm start
. Next, open a new terminal window to start our web app. Type npm start
inside the web
directory. You should see the two books on screen that our server returned!
Now we can take a step back and compare a REST request like we did in the first post. This time we understand how a real query looks with real code.
A REST call for this same data may have looked like this inside of a React component:
import { useState, useEffect } from 'react';
const useBooksRequest = () => {
let [books, setBooks] = useState();
useEffect(() => {
const getBooks = async () => {
let response = await fetch('http://localhost:3000/books');
let books = await response.json();
setBooks(books);
};
getBooks();
}, [setBooks]);
return { books };
};
This would be a hook that runs when the component mounts, and sets the state of books when the response comes back. It would look very similar to our current Books:
const Books = () => {
let { books } = useBooksRequest();
if (!books) return null;
return books.map(book => (
<div>
<h3>{book.title}</h3>
<p>{book.author}</p>
</div>
));
};
So what are we getting out of using Apollo Client? Some of this goes back to that first article. But there is a lot more I can talk about this time around.
Comparing Redux / REST
Imagine if we had a delete book REST call. And also imagine that we have the latest book title in the navbar at the top of our site. This means, if we are on our books page, and we delete the newest book, what needs to happen?
- Call delete book endpoint
- Remove the book from the books array in our state
- If that book was the first book, we need to update the nav bar, since it is now deleted
Now let’s imagine we also care about showing a loading spinner while the delete is taking place. If we are using something like Redux, we are probably firing off this stuff in order:
- START_LOADING
- DELETE_BOOK
- SET_NAVBAR_BOOK_TITLE
- STOP_LOADING
At this point, this is feeling pretty complicated. This is where Apollo client comes in, and makes a huge difference. All of this code that you’d be used to writing with redux is no longer needed. Let’s see how.
In web/src
let’s make a new file called Nav.js
.
import React from 'react';
import useBooksQuery from './pages/useBooksQuery';
const Nav = () => {
let { data } = useBooksQuery();
if (!data || !data.books) return null;
return (
<div>
This is our amazing nav bar. The latest book in our collection is{' '}
{data.books[0].title}
</div>
);
};
export default Nav;
Next, we can edit src/App.js
to render the nav at the top:
import React from 'react';
import Nav from './Nav';
import apolloClient from './apolloSetup';
import { ApolloProvider } from '@apollo/react-hooks';
import Books from './pages/Books';
const App = () => (
<ApolloProvider client={apolloClient}>
<React.Fragment>
<Nav />
<Books />
</React.Fragment>
</ApolloProvider>
);
export default App;
Now, if we load up the page, you’ll see that text at the top. Once again, I am trying to demonstate an area where some state is needed in two different places, in a global element (nav bar) and on a single screen (books edit page). This is very realistic for many apps. You could imagine an edit user screen and showing user info in the nav, for instance.
The Benefits of Redux, With Less Code
Let’s go step by step showing each piece of the puzzle.
Adding the delete mutation
First, we need to add a delete mutation to our server. We will make this really simple and fake deleting a book. What I mean by this is, our server will respond saying the book is deleted, but refreshing the page will bring it back.
Let’s open up server/src/resolvers.js
and add a mutation at the bottom:
Mutation: {
//existing one
addAuthor: (_, { input: { name, twitter } }) => {
return {
name,
twitter,
};
},
deleteBook: (_, { title }) => true,
},
Next, open up typeDefs
and add this to the existing Mutation type:
type Mutation {
...
deleteBook(title: String!): Boolean
}
Now we can restart our server. What we just did will add a mutation that takes a book title, and returns true every time. Realistically, we would want a book id
to be sent, and then actually delete it from our database.
Adding the mutation to the frontend
Let’s make useDeleteBookMutation.js
inside web/src/pages
:
import gql from 'graphql-tag';
import { useMutation } from '@apollo/react-hooks';
export const mutation = gql`
mutation DeleteBook($title: String!) {
deleteBook(title: $title)
}
`;
export default () => {
let [deleteBook] = useMutation(mutation);
return deleteBook;
};
You’ll notice this time we are importing useMutation
instead of useQuery
. The rest is pretty straight forward. This hook will give us a mutate
function that we can call when we want to hit the server. Let’s open up Books.js
and import it:
import React from 'react';
import useBooksQuery from './useBooksQuery';
import useDeleteBookMutation from './useDeleteBookMutation';
const Books = () => {
let { data } = useBooksQuery();
let deleteBook = useDeleteBookMutation();
if (!data || !data.books) return null;
return data.books.map(book => (
<div>
<h3>{book.title}</h3>
<p>{book.author}</p>
<button onClick={() => deleteBook({ variables: { title: book.title } })}>
Delete Book
</button>
</div>
));
};
export default Books;
At this point, if we load up our app, and hit delete, it hits the server, but nothing happens. This is expected. Since we are returning only a boolean, it’s up to us to tell Apollo what to do next.
Updating the local store
We can update the local cache after any request for immediate UI updates. Here’s how it looks, if we edit the button
code in Books
:
// Update the import at the top for `useBooksQuery to be:
import useBooksQuery, { query } from './useBooksQuery';
...
<button
onClick={() =>
mutate({
variables: { title: book.title },
update: store => {
const data = store.readQuery({
query,
});
store.writeQuery({
query,
data: {
books: data.books.filter(
currentBook => currentBook.title !== book.title,
),
},
});
},
})
}
>
Delete Book
</button>
Well… that looks like a lot doesn’t it! If we run it, and click delete, you will notice that the navbar and the list of books both automatically update with the correct data, having the book removed. Before I explain this further, let’s refactor it slightly. Change Books
to this:
import React from 'react';
import useBooksQuery from './useBooksQuery';
import useDeleteBookMutation from './useDeleteBookMutation';
const Books = () => {
let { data } = useBooksQuery();
let deleteBook = useDeleteBookMutation();
if (!data || !data.books) return null;
return data.books.map(book => (
<div>
<h3>{book.title}</h3>
<p>{book.author}</p>
<button onClick={() => deleteBook(book.title)}>Delete Book</button>
</div>
));
};
export default Books;
You’ll notice that deleteBook
is only taking a book title. We can update useDeleteBookMutation
to do all of the logic we previously had in our render. This keeps our Books
much more readable. Open useDeleteBookMutation
and replace it with:
import gql from 'graphql-tag';
import { useMutation } from '@apollo/react-hooks';
import { query as booksQuery } from './useBooksQuery';
export const mutation = gql`
mutation DeleteBook($title: String!) {
deleteBook(title: $title)
}
`;
export default () => {
let [deleteBook] = useMutation(mutation);
return title => {
return deleteBook({
variables: { title },
update: store => {
const data = store.readQuery({
query: booksQuery,
});
store.writeQuery({
query: booksQuery,
data: {
books: data.books.filter(
currentBook => currentBook.title !== title,
),
},
});
},
});
};
};
Ahh, this looks much better. Our GraphQL logic is now co-located with its query. As a developer it is much easier to come in and see what is going on by keeping this section together.
So, how does it work? Any mutation can have an optional update
function. It gets the store (local cache from all queries) and the result of the mutation as a second parameter, which we are not using. Since we want to update the UI after this mutation, we first read a query:
const data = store.readQuery({
query: booksQuery,
});
This gives us the data just like running a query with useQuery
except it is grabbed from our InMemoryCache
that we setup. Next, all we do is tell apollo to write a new object to the store for that query:
store.writeQuery({
query: booksQuery,
data: {
books: data.books.filter(currentBook => currentBook.title !== title),
},
});
In the code above, I am telling apollo to update the booksQuery
data to filter out any book that has the title we passed into the mutation.
Loading indicators
Apollo also gives us a loading indicator built in to our queries. If you want to show a spinner or some other UI element when something is loading, it would look like this, inside Books.js
const Books = () => {
let { data, loading, refetch } = useBooksQuery();
let mutate = useDeleteBookMutation();
if (loading) return <div>loading...</div>;
if (!data || !data.books) return null;
return data.books.map(book => (
<div>
<h3>{book.title}</h3>
<p>{book.author}</p>
<button onClick={() => refetch()}>Reload Books</button>
<button onClick={() => mutate(book.title)}>Delete Book</button>
</div>
));
};
In this example, I even threw in the refetch function. By default, the first time this component loads, it will show a loading indicator. If we trigger a refetch, it will load it behind the scenes, and not show the loading div. This is something that can be changed if needed. You can pass notifyOnNetworkStatusChange
to our deleteBook
mutation if you wanted it to set loading to true during refetches:
return deleteBook({
variables: { title },
notifyOnNetworkStatusChange: true
...
})
Apollo also gives us the flexibility to choose when to automatically refetch data. The default will load it once, then use the cache, until you manually call refetch, or in our case, reloading the page since we are using the in memory cache. You can specify other policies, as seen here. In a typical redux type application, or even the fetch
example I wrote above, you wouldn’t have this level of control without bringing in another library. You would always be fetching when the component mounted.
Automatic Updates
You may be wondering… can Apollo be smarter? With the GraphQL type system, it can. We just need to do a couple easy things before I can show you this in action.
Open resolvers
in the server folder, and let’s add some unique id’s to the books:
books: () => {
return [
{
id: 1,
title: 'Harry Potter and the Chamber of Secrets',
author: 'J.K. Rowling',
},
{
id: 2,
title: 'Jurassic Park',
author: 'Michael Crichton',
},
];
};
Open typeDefs
and add it to the book type:
type Book {
id: Int!
title: String!
author: String!
}
Restart the server, and open our useBooksQuery
in the web folder, and add it to the query:
books {
id
title
author
}
Add Change Book Mutation
Cool, we are all set. Apollo client is smart enough to see something of the same type (Book) and the same id (1, or 2 in our case) it will update the local cache. So if we make a new resolver, called changeBookTitle
in server/resolvers.js
we can see this in action. Replace the contents of resolvers
:
const books = [
{
id: 1,
title: 'Harry Potter and the Chamber of Secrets',
author: 'J.K. Rowling',
},
{
id: 2,
title: 'Jurassic Park',
author: 'Michael Crichton',
},
];
export const resolvers = {
Query: {
books: () => {
return books;
},
authors: () => {
return [
{ name: 'Todd', twitter: 'toddmotto' },
{ name: 'React', twitter: 'reactjs' },
];
},
},
Mutation: {
addAuthor: (_, { input: { name, twitter } }) => {
return {
name,
twitter,
};
},
deleteBook: (_, { title }) => true,
changeBookTitle: (_, { input }) => {
let { id, title } = input;
let book = books.find(book => book.id === id);
//Return the new book title
return {
...book,
title,
};
},
},
};
I pulled out the books array so that we can mimic finding it in our database or elsewhere, and then returning the book object with the new title we pass in.
Now, we need to update our typeDefs
:
input ChangeBookInput {
id: Int!
title: String!
}
type Mutation {
changeBookTitle(input: ChangeBookInput!): Book
...
}
Create client side mutation
Our changeBookTitle
resolver will take a book id, and a title, and return a Book object. Now we can create the client side query. Let’s name it src/pages/useChangeBookTitleMutation.js
import gql from 'graphql-tag';
import { useMutation } from '@apollo/react-hooks';
export const mutation = gql`
mutation ChangeBookTitle($input: ChangeBookInput!) {
changeBookTitle(input: $input) {
id
title
}
}
`;
export default () => {
let [mutate] = useMutation(mutation);
return ({ id, title }) => {
return mutate({
variables: { input: { id, title } },
});
};
};
Hopefully you’re starting to see the pattern here.
- Create a typeDef for a resolver
- Make the resolver
- Create the client side query / mutation
- Use it!
Let’s make an input field called ChangeTitle.js
inside src/pages
:
import React, { useState } from 'react';
import useChangeBookTitleMutation from './useChangeBookTitleMutation';
const ChangeTitle = ({ book }) => {
let changeTitle = useChangeBookTitleMutation();
let [title, setTitle] = useState(book.title);
return (
<div>
<input value={title} onChange={e => setTitle(e.target.value)} />
<button onClick={() => changeTitle({ id: book.id, title })}>
Change it!
</button>
</div>
);
};
export default ChangeTitle;
Now, let’s add it to Books.js
:
import React from 'react';
import useBooksQuery from './useBooksQuery';
import useDeleteBookMutation from './useDeleteBookMutation';
import ChangeTitle from './ChangeTitle';
const Books = () => {
let { data } = useBooksQuery();
let mutate = useDeleteBookMutation();
if (!data || !data.books) return null;
return data.books.map(book => (
<div>
<h3>{book.title}</h3>
<p>{book.author}</p>
<button onClick={() => mutate(book.title)}>Delete Book</button>
<ChangeTitle book={book} />
</div>
));
};
export default Books;
Phew! We did it. If you change some text in the first input field, and hit save. You will see that the title is automatically updated in the navbar, and the book itself. We didn’t have to do any state updates ourself.
This is why I love Apollo / GraphQL. Before, I would need a state management system that was a very manual process for doing anything, and it was a lot of code. Now, I will use Apollo for everything, and React Context, or Apollo’s local resolvers for local state.
Conclusion
To recap, we covered setting up Apollo in a React frontend. We compared it to a REST endpoint now that we can see all of the benefits. We also learned that the intelligent updating due to the type system, and ease of manual store updates, cuts down on the UI logic we had to write in the past.
You can clone the course repo and git checkout 3-client-integration-end
to see the final code if you made a mistake along the way.