GraphQL Subscriptions in Apollo Client
Amanda Liu
In the last few months, we’ve implemented a lot of exciting features in Apollo Client, including pagination and infinite scrolling, optimistic UI, and query batching. Today we’re adding another one: GraphQL subscriptions!
In this post, we’ll explain the design and implementation in detail, but if you just want to see the code scroll to the end for links to all of our packages and examples!
GraphQL subscriptions allow developers to introduce new levels of interactivity to their apps with near-realtime updates. You can keep your app updated to the latest changes (that you subscribe to) between different users:
The Design
Our GraphQL subscriptions implementation leverages existing publish-subscribe systems, like the design Facebook uses for their production apps, according to their conference talks.
Clients specify the data they would like to subscribe to by sending a query with the subscription keyword to the server:
subscription comments($repoName: String!) { newComments(repoName: $repoName) { content postedBy { username } postedAt } }
Based on the subscription field name “newComments” and a mapping from subscription names to channels, the server subscribes to one or more pub/sub channels. When something is published to one of these channels, the server runs the GraphQL query specified in the subscription and sends a full new result to the client.
On the client the developer specifies the name of the subscription and any relevant arguments, and on the server the developer specifies rules for when that subscription should re-run. For instance, on a social media website, a client can subscribe to data changes such as new comments, edited posts, and more.
Subscriptions — unlike queries or mutations — can deliver more than one result, so a long-lived connection is necessary. In our implementation, we use a WebSocket connection to implement a basic subscription protocol. The npm package we created for this is completely independent from Apollo Client or Apollo Server, and can be used with any JavaScript client and Node.js server.
Publish-subscribe
GraphQL subscriptions are based on a simple publish-subscribe system. In our server-side subscriptions package, when a client makes a subscription, we simply use a map from one subscription name to one or more channel names to subscribe to the right channels. The subscription query will be re-run every time something is published to one of these channels. We think a common pattern will be to publish mutation results to a channel, so a subscription can send a new result to clients whenever a mutation happens. This is why some people call subscriptions the result of someone else’s mutation.
The diagram below details the life of subscription getNewestComment, and how it is triggered during the submitComment mutation by publishing to the submitComment channel.
The Implementation
To demonstrate how to wire our web socket GraphQL subscription package with Apollo Client and a JavaScript GraphQL server, we’ll go through how subscriptions work in GitHunt, our example app for Apollo Client. If you want to see the exact changes we made to add subscriptions, check out the pull requests for the client and the server.
We now merged and released updated versions of all libraries and docs, to check these out looks at those links:
Client
To take advantage of subscriptions in Apollo, we made an npm package called subscriptions-transport-ws, which implements a WebSocket subscription protocol. We can use that package to add subscription capabilities when we create the network interface used by Apollo Client:
import { Client, addGraphQLSubscriptions } from 'subscriptions-transport-ws';const wsClient = new Client('ws://localhost:8080');const networkInterface = createNetworkInterface({ uri: '/graphql', opts: { credentials: 'same-origin', }, });const networkInterfaceWithSubscriptions = addGraphQLSubscriptions( networkInterface, wsClient, );const client = new ApolloClient({ networkInterface: networkInterfaceWithSubscriptions, // ... });
We will use a subscription called commentAdded as an example. Our subscription listens for new comments on the comments page for a specific repository. On the client, we use the subscription API of Apollo Client to set up the subscription when the CommentsPage component mounts:
componentDidMount() { const repoName = this.props.entry.repository.full_name; const updateQueryFunction = this.props.updateCommentsQuery; this.subscribe(repoName, updateQueryFunction); }subscribe(repoName, updateQuery){ // call the "subscribe" method on Apollo Client this.subscriptionObserver = this.props.client.subscribe({ query: SUBSCRIPTION_QUERY, variables: { repoFullName: repoName }, }).subscribe({ next(data) { // ... call updateQuery to integrate the new comment // into the existing list of comments }, error(err) { console.error('err', err); }, }); }
ApolloClient.subscribe takes a query and variables, and returns an observable. We then call subscribe on the observable, and give it a next function which will call updateQuery. updateQuery specifies how we want our query result to be updated when given the subscription result. Note that this is very similar to how updateQueries works in Apollo Client’s mutate function, as detailed here.
When we’re no longer interested in data, we simply call unsubscribe on the observable, which will stop the subscription.
Server
On the server we need to start a WebSocket server that implements the subscriptions protocol from the subscriptions-transport-ws package. To make each part as reusable as possible, we separated the GraphQL and publish-subscribe parts of our implementation from the transport implementation.
The part that actually handles the subscriptions is in a package we called graphql-subscriptions. From that package, we import SubscriptionManager and pass it to the WebSocket server as an argument:
import { PubSub, SubscriptionManager } from 'graphql-subscriptions'; import schema from './schema';// the default PubSub is based on EventEmitters. It can easily // be replaced with one different one, e.g. Redis const pubsub = new PubSub();const subscriptionManager = new SubscriptionManager({ schema, pubsub, setupFunctions: { commentAdded: (options, args) => ({ commentAdded: comment => comment.repository_name === args.repoFullName, }), }, });export { subscriptionManager, pubsub };
setupFunctionsis a core part of the subscriptions API on the server: it’s a map of subscription names to channel names and filter functions. In our example, the commentAdded subscription subscribes to a channel of the same name, and runs the subscription query whenever the repository name of the new comment is the same and the repoFullName argument is provided to the subscription.
The subscriptionManager can work with any pub-sub package that exposes publish, subscribe and unsubscribe methods. In our example, we built a very simple package based on Node event emitters. If you want to use subscriptions in production, you should use David Yahalomi’s excellent package based on Redis instead, which he’ll explain in an upcoming post.
Next, we simply need to import the same PubSub instance and publish to the commentAdded channel when we run the submitComment mutation. We use the value returned by the mutation resolver as the payload, which gets passed directly into the subscription query as the GraphQL rootValue. If you follow that convention, a subscription really is the result of somebody else’s mutation!
submitComment: (_, { repoFullName, commentContent }, context) => { return Promise.resolve() .then(() => ( // Save comment to DB context.Comments.submitComment( repoFullName, context.user.login, commentContent ) )) .then(([id]) => // Get new comment to return context.Comments.getCommentById(id) ) .then(comment => { // Publish subscription notification with the whole comment pubsub.publish('commentAdded', comment); return comment; }); }, }
With the subscriptionManager and the publication set up, all we need to do is wire it up to our WebSocket server and start it!
import { createServer } from 'http'; import { Server } from 'subscriptions-transport-ws';const httpServer = createServer((request, response) => { response.writeHead(404); response.end(); });httpServer.listen(WS_PORT, () => console.log( `Websocket Server is now running on http://localhost:${WS_PORT}` ));const server = new Server({ subscriptionManager }, httpServer);
That’s it; now our GraphQL subscriptions are all set up to listen to new comments! All in all, it’s just a few new lines on the client and server. You can see exactly what we did in the pull requests for the client and for the server.
Now that you know how it all works, you can jump right in and see if you can build something great with the packages and examples we provide:
- graphql-subscriptions: the server side package that connects GraphQL with a publish-subscribe system.
- subscriptions-transport-ws: the package implementing a WebSocket transport protocol for subscriptions
- GitHunt-React: the client of our example app, which now includes subscriptions
- GitHunt-API: the server of our example app, including subscriptions.
- Subscriptions React docs: How to handle subscriptions in your React app
- Subscriptions GraphQL Server docs: How to add GraphQL Subscriptions to your Node server
We’re only just getting started with subscriptions in GraphQL — there’s still a lot to do and we’re always looking for contributors and ideas. To get started, just try out the examples, build one of your own and let us know about it, or file an issue or PR on one of our repos!