Now that we’ve completed the lifecycle of GraphQL by building out a server and using queries on the frontend, it’s time to do some more interesting things and dive into subscriptions and some websockets.
For those unfamiliar, websockets are a way to instantly update your user interface. For example, Google Analytics has a real-time active users feature, if a new user joins your website then the count will increase and if a user leaves the count will decrease. With websockets, you can send out an update to anyone currently on your application, and change the UI, without a page refresh.
Table of contents
In this post, we’ll be making some GraphQL Subscriptions over websockets. Real-time programming can be a whole lot of fun. With Apollo Server and the GraphQL spec, websockets are easier to work with. If you have an understanding of GraphQL, there’s not much new to learn, other than the required server setup.
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
You may remember from our first post where I mentioned that the same query / mutation syntax can be used for writing subscriptions. This is the big selling point, for anyone with websocket experience. Websocket logic typically is handled by a separate library on the frontend. You’d need to make a REST api call, and then pull in a websocket library.
Luckily for us, we’re able to stay inside our GraphQL layer, without having to write different code to support websockets. This is because the GraphQL spec includes subscriptions, and keeps the same query syntax you should now be used to!
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
Server Setup
We’ll start by doing:
git checkout 4-start
from the course github repo.- Open up the
server
folder - Run
npm install
I have updated some dependencies and made hot reloading work better. With that out of the way, we can get started.
Let’s create a file called server/src/pubsub.js
:
import { PubSub } from 'apollo-server';
const pubsub = new PubSub();
export default pubsub;
This is the module we will call anytime we want to publish some data over our websocket connection. You can think of it this way; anytime an action takes place on your website, and you want to live update anyone currently using your application, we would use this pubsub module.
Next, let’s add our first subscription type. Open up server/src/typeDefs.js
and add the following:
type Subscription {
bookTitleChanged: Book
}
Since we made it possible to change a book title in the UI, we can make this live update in other people’s browsers. In this case, the idea is that anyone looking at your site will immediately see the book title change after someone changes it. A more real-world scenario for this could be blog posts on a popular site. If you are sitting on the homepage, new posts could fade in the moment they are posted.
To make this work, we only have one file left to edit on the server. Open up server/src/resolvers.js
.
I have the full resolvers.js
below, so that you can confirm you did it correctly.
Add this import to the top:
import pubsub from './pubsub';
Then, inside changeBookTitle
add this, below the book:
pubsub.publish("bookTitleChanged", {
bookTitleChanged: { ...book, title }
});
And then finally, the subscription itself needs to go after the `Mutation` object:
Subscription: {
bookTitleChanged: {
subscribe: () => pubsub.asyncIterator(["bookTitleChanged"]);
}
}
That may have been a little confusing if you’re new to JS in general. So here is the fully updated resolvers.js
:
import pubsub from './pubsub';
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);
// Publish the book so that the subscription can fire off
pubsub.publish('bookTitleChanged', {
bookTitleChanged: { ...book, title },
});
//Return the new book title
return {
...book,
title,
};
},
},
// Add Subscription at the end of the resolvers object
Subscription: {
bookTitleChanged: {
subscribe: () => pubsub.asyncIterator(['bookTitleChanged']),
},
},
};
The flow is pretty straight forward. Whenever you want a subscription to run, you publish some data to it. In this case, my subscription is listening for bookTitleChanged
. Whenever that gets published, any subscribers will receive that data. You can read more about this on the official docs.
I should also mention that it is possible to subscribe to specific things. If you wanted to only get books that changed if the book had a certain id
. You could do this. It’s a little out of the scope of this tutorial, but the docs link above goes over Subscription Filters
.
Client Setup
With that finished, our server is ready for subscriptions. Now, we get to update our client code to support subscriptions. Open up the web
folder and follow along:
First, we need to install the required websocket packages for Apollo Client:
npm install --save apollo-link-ws subscriptions-transport-ws
Inside web/src/apolloSetup.js
we create a websocket link:
import { WebSocketLink } from 'apollo-link-ws';
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/`,
options: {
reconnect: true,
},
});
Unfortunately, after that, we have to import some other special things that will tell Apollo to route any subscription requests through the websocket link we just added. This looks a little confusing, so bare with me and replace the contents of apolloSetup.js
:
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { ApolloClient } from 'apollo-client';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
import { InMemoryCache } from 'apollo-cache-inmemory';
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/graphql`,
options: {
reconnect: true,
},
});
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
});
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
export default new ApolloClient({
cache: new InMemoryCache(),
link,
});
This is mostly taken from the official doc if you’re curious. It says, if the operation is a subscription, use the websocket link, otherwise, use the normal httpLink. Websockets work by using a constant connection to the server. This isn’t possible over http, because http requests do not stay open.
useSubscription
With that setup out of the way, we can finally write the request!
Create src/useBookTitleChanged.js
:
import gql from 'graphql-tag';
import { useSubscription } from '@apollo/react-hooks';
export const subscription = gql`
subscription BookTitleChanged {
bookTitleChanged {
id
title
author
}
}
`;
export default () => useSubscription(subscription);
Open up Books.js
and add the following:
import useBookTitleChanged from "./useBookTitleChanged";
const Books = () => {
useBookTitleChanged();
...
Adding this hook to the top of the component will make everything work as expected. Open up two browser windows, and change the title in one of them.
You’ll see the other window updates the title as well. One problem you may notice however, is that the input field doesn’t update. This is an easy fix. If we open up ChangeTitle.js
and add the following, that will update as well:
import React, { useState, useEffect } from 'react';
import useChangeBookTitleMutation from './useChangeBookTitleMutation';
const ChangeTitle = ({ book }) => {
let changeTitle = useChangeBookTitleMutation();
let [title, setTitle] = useState(book.title);
//new code
useEffect(() => {
setTitle(book.title);
}, [setTitle, book]);
//end new code
return (
setTitle(e.target.value)} />
changeTitle({ id: book.id, title })}>
Change it!
);
};
export default ChangeTitle;
I will call out one more problem, a user may be in the middle of editing the field, in which case we wouldn’t want the input to be overwritten. You could prevent the update in the useEffect if the input is currently focused, or not worry about this at all since what we created isn’t too real-world of a problem.
I am going off on a bit of a tangent here to show the sorts of problems you could run into if you carelessly updated your api from a websocket.
Conclusion
If you got lost along the way, you can git checkout 4-end
to see the final code. I hope this gave you a fun introduction to subscriptions inside GraphQL. After the initial setup, it can be quick and easy to add subscriptions into your app.
In the next post, we will take our current code, and show how easy it is to get automatic TypeScript generation thanks to the GraphQL schema. We’re getting close to the end of the series, and I hope you’re learning a lot. Before the next one, try doing some other things with subscriptions. Maybe a live chart, or user count for who’s currently on the page. I’m sure you can come up with other neat ideas!