Subscriptions are a powerful GraphQL feature that make it easy to receive updates from a backend server in real time using a technology like WebSockets on the frontend. In this quick post we’ll go over how to setup the frontend for subscriptions in Angular using Apollo Client 2.0.
For the examples is this post, we’ll assume that you already have a GraphQL server up and running with subscriptions properly setup on the server-side. You can easily setup your own GraphQL server using a tool like graphql-yoga, or you can use the Graphcool framework and have Graphcool host your GraphQL service.
We’ll assume that you’ll want to build an app that has both GraphQL subscriptions as well as regular queries and mutations, so we’ll set things up with both a regular HTTP link and a WebSocket link. split, an utility from the apollo-link package, will make it easy to direct requests to the correct link.
First, we’ll need a whole bunch of packages to make everything work: apollo-angular, apollo-angular-link-http, apollo-cache-inmemory, apollo-link, apollo-link-ws, apollo-utilities, graphql, graphql-tag, apollo-client and subscriptions-transport-ws.
Let’s install all of them at once using npm or Yarn:
$ npm i apollo-angular apollo-angular-link-http apollo-cache-inmemory apollo-link apollo-link-ws apollo-utilities graphql graphql-tag apollo-client subscriptions-transport-ws
# or, using Yarn:
$ yarn add apollo-angular apollo-angular-link-http apollo-cache-inmemory apollo-link apollo-link-ws apollo-utilities graphql graphql-tag apollo-client subscriptions-transport-ws
We’ll create a module with our Apollo configuration and links. Let’s call it GraphQLConfigModule:
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { Apollo, ApolloModule } from 'apollo-angular';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
@NgModule({
exports: [HttpClientModule, ApolloModule, HttpLinkModule]
})
export class GraphQLConfigModule {
constructor(apollo: Apollo, private httpClient: HttpClient) {
const httpLink = new HttpLink(httpClient).create({
uri: 'REGULAR_ENDPOINT'
});
const subscriptionLink = new WebSocketLink({
uri:
'___SUBSCRIPTION_ENDPOINT___',
options: {
reconnect: true,
connectionParams: {
authToken: localStorage.getItem('token') || null
}
}
});
const link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
subscriptionLink,
httpLink
);
apollo.create({
link,
cache: new InMemoryCache()
});
Here are a few things to note about our configuration:
At this point, if the TypeScript compiler complains with something like Cannot find name 'AsyncIterator'
, you can add esnext to the list of libs in your tsconfig.json file:
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
...,
"lib": [
"es2017",
"dom",
"esnext"
]
}
}
With this configuration module in place, all that’s left to do for our setup is to import the module in our main app module:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { GraphQLConfigModule } from './apollo.config';
import { AppComponent } from './app.component';
We’re now ready to subscribe to different events in the frontend. Here’s a simple subscription query that we’ll run to automatically receive new todos created on the server:
subscription newTodos {
Todo(filter: { mutation_in: [CREATED] }) {
node {
title
description
completed
}
}
}
For a real app, you’ll probably want to decouple your subscription logic in a service, but here we’ll do everything is our app component’s class for the sake of simplicity:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';
const subscription = gqlsubscription newTodos {
Todo(filter: { mutation_in: [CREATED] }) {
node {
title
description
completed
}
}
};
interface TodoItem {
title: string;
name: string;
completed: boolean;
}
@Component({ ... })
export class AppComponent implements OnInit, OnDestroy {
todoSubscription: Subscription;
todoItems: TodoItem[] = [];
constructor(private apollo: Apollo) {}
ngOnInit() {
this.todoSubscription = this.apollo
.subscribe({
query: subscription
})
.subscribe(({ data }) => {
this.todoItems = [...this.todoItems, data.Todo.node];
});
}
Notice how it’s very similar to running a regular GraphQL query. With this in place, our frontend app will automatically receive new todo items.
We can display our todo items with something like this in our component’s template:
<ul>
<li *ngFor="let item of todoItems">{{ item.title }} - {{ item.description }}</li>
</ul>
Our example so far works well, but our app only gets new todos. As soon as we refresh, we get an empty list of todo items once again.
The simple solution is to first run a regular query and then use the subscription to receive additional todos automatically. That’s easy to do using the subscribeToMore method on an initial watchQuery:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { Apollo, QueryRef } from 'apollo-angular';
import gql from 'graphql-tag';
const subscription = gqlsubscription newTodos {
Todo(filter: { mutation_in: [CREATED] }) {
node {
title
description
completed
}
}
};
const allTodosQuery = gqlquery getTodos {
allTodos {
title
description
completed
}
};
interface TodoItem {
title: string;
description: string;
completed: boolean;
}
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
todoSubscription: Subscription;
todoItems: TodoItem[] = [];
todoQuery: QueryRef<any>;
constructor(private apollo: Apollo) {}
ngOnInit() {
this.todoQuery = this.apollo.watchQuery({
query: allTodosQuery
});
this.todoSubscription = this.todoQuery.valueChanges.subscribe(
({ data }) => {
this.todoItems = [...data.allTodos];
}
);
this.setupSubscription(); }
setupSubscription() {
this.todoQuery.subscribeToMore({
document: subscription,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) {
return prev;
}
const newTodo = subscriptionData.data.Todo.node;
return Object.assign({}, prev, {
allTodos: [...prev['allTodos'], newTodo]
});
}
}); }
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
I have a problem with the
app.component.ts
example. On line 2:import { Subscription } from 'rxjs/Subscription';
I’m using rxjs v6.5 and there is no Subscription export.