Tutorial

GraphQL Subscriptions in Angular using Apollo 2.0

Angular

While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or edited it to ensure you have an error-free learning experience. It's on our list, and we're working on it! You can help us out by using the "report an issue" button at the bottom of the tutorial.

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';

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 = 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:

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 = 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]
    });
  }
});  }

  • 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.

0 Comments

Creative Commons License