GraphQL Subscriptions in Angular using Apollo 2.0

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.

Required Packages

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

Setup

We’ll create a module with our Apollo configuration and links. Let’s call it GraphQLConfigModule:

apollo.config.ts

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:

  • In our module’s constructor, we first define an HTTP link using the apollo-angular-link-http package. Internally our link uses Angular’s HttpClient.
  • We then also define a WebSocket link with our GraphQL server’s subscription endpoint. Here you can see that we also get an authorization token from localStorage to authenticate our WebSocket connections. You’ll need this if your server only accepts authenticated subscription requests.
  • Next we use the split utility, which takes the query and returns a boolean. Here we check the query using another utility called getMainDefinition to extract the kind of query and operation name. From there, we can check if the query is a subscription and, if so, use the subscription link. Otherwise, the request will use the HTTP link.
  • Finally we simply create the link and make use of InMemoryCache for caching.

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:

tsconfig.json

{
  "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:

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { GraphQLConfigModule } from './apollo.config';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, GraphQLConfigModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Simple Subscription

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:

app.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';

import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';

const subscription = gql`
  subscription 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];
      });
  }

  ngOnDestroy() {
    this.todoSubscription.unsubscribe();
  }
}

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:

app.component.html

<ul>
  <li *ngFor="let item of todoItems">{{ item.title }} - {{ item.description }}</li>
</ul>

watchQuery + Subscriptions

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:

app.component.ts

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 = gql`
  subscription newTodos {
    Todo(filter: { mutation_in: [CREATED] }) {
      node {
        title
        description
        completed
      }
    }
  }
`;

const allTodosQuery = gql`
  query 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]
        });
      }
    });
  }

  ngOnDestroy() {
    this.todoSubscription.unsubscribe();
  }
}
  • We first setup a watchQuery and subscribe to its valueChanges observable to get all the todo items when the component initializes.
  • We then setup our subscription by using subscribeToMore on our watchQuery.
  • subscribeToMore takes a query document (our subscription query), variables if needed and an update query function that gets the data from the previous query and an object that contains our subscription data (subscriptionData).
  • If the subscription data is empty we simply return the previous data, but if not we construct and return a new object that contains our previous data with our new data.
  Tweet It
✖ Clear

🕵 Search Results

🔎 Searching...