Using Subscriptions to Update Data in Relay Modern

 
Code on a computer

Built With

This post was written using:

  • React 16.8.x
  • Relay 4.0.x
  • GraphQL Spec - June 2018
  • NodeJS 8.x LTS

Overview

While working on a Relay project, I ran across a common application pattern that can be hard to understand if you're new to GraphQL and Relay: using GraphQL Subscriptions to update a client that already has data from an initial GraphQL Query. Imagine you're building a simple chatroom or collaborative Todo List, for example, that initially fetches data from a server then uses websockets to keep the React UI up to date in real time.

Since Relay is a fairly new and evolving framework finding good examples of subscription patterns was difficult. Figuring out how to implement this pattern for the first time took me several hours of combing through documentation and trial and error, so I hope to save others this headache by providing a straightforward example.

If you follow Relay's requirements for GraphQL data structures, subscriptions can easily be used to supplement existing GraphQL queries. As new data is received it can be inserted into the Relay store, allowing your UI to update based on the new data without any rendering awareness of subscriptions.

This post will walk you through building a basic React component that mocks a simple chat interface using Relay Modern, fetching initial data with a GraphQL Query, then receiving updates using a GraphQL Subscription. You'll start with a vanilla create-react-app application and install Relay and other necessary tools along the way.

For an introduction to GraphQL, check out HowToGraphQL and for an intro to the Relay framework, see Introduction to Relay. Relay is a performance-oriented framework that couples React with GraphQL.

1. Prerequisites

First we need a GraphQL server to connect our Relay app to, and more importantly, we need the server's schema so that Relay understands the data the server can provide.

A GraphQL Server

The details of setting up a GraphQL server are beyond the scope of this tutorial, but a simple server is provided for demo purposes. Clone the server from https://gitlab.com/brewcore/graphql-relay-subscription-demo-server and run npm install followed by npm start to spin up the server for this demo.

Take a look at the GraphQL schema below to see what the server offers, and open the graphql explorer at http://localhost:4001/graphql to experiment with the server.

"""A new chat message"""
type newMessage {
  id: ID!

  """The name of the user that sent the message"""
  username: String!

  """The message"""
  message: String!
}

"""A connection to a list of items."""
type newMessageConnection {
  """Information to aid in pagination."""
  pageInfo: PageInfo!

  """A list of edges."""
  edges: [newMessageEdge]
}

"""An edge in a connection."""
type newMessageEdge {
  """The item at the end of the edge"""
  node: newMessage

  """A cursor for use in pagination"""
  cursor: String!
}

"""Information about pagination in a connection."""
type PageInfo {
  """When paginating forwards, are there more items?"""
  hasNextPage: Boolean!

  """When paginating backwards, are there more items?"""
  hasPreviousPage: Boolean!

  """When paginating backwards, the cursor to continue."""
  startCursor: String

  """When paginating forwards, the cursor to continue."""
  endCursor: String
}

"""Root query type"""
type Query {
  """
  Messages in the mock chatroom - note that pagination is not implemented since this is just a demo
  """
  messages(after: String, first: Int, before: String, last: Int): newMessageConnection
}

"""Subscribe to data event streams"""
type Subscription {
  """New chat messages"""
  messages: newMessageEdge
}

This schema has two important parts: a Query that allows the client to fetch a paginated list of Messages, and a Subscription that allows the client to receive new Messages in the same format as the original paginated list. The schema follows Relay's GraphQL specification, implementing nodes, cursors, and edges to standardize our data for Relay.

Creating an app with create-react-app

Next, create a new React application using create-react-app, a helpful tool for creating React applications.

# If necessary install `create-react-app`
npm install -g create-react-app

# Create a new React app called `relay-client` (and navigate into it)
create-react-app relay-client
cd relay-client

2. Setting up Relay

Your next step is to integrate Relay with React and configure the Relay environment, so Relay knows how to talk to the GraphQL API.

Install Relay Dependencies

First you need to install some dependencies for Relay. Run the following commands:

cd relay-client
npm install --save react-relay
npm install --save-dev babel-plugin-relay graphql relay-compiler

Next, add this line to the scripts section of package.json. This gives you an easy way to run Relay's compiler.

"relay": "relay-compiler --src ./src --schema ./schema.graphql"

Relay requires the use of a compiler to convert graphql statements to optimized runtime code. For more info about the Relay compiler see https://relay.dev/docs/en/graphql-in-relay.html#relay-compiler.

Configure a Relay Environment

A Relay environment brings together everything Relay needs to function, which is at a minimum a Store which handles data storage and caching, and a Network Layer which handles communications with the GraphQL server.

Create a new file at src/Relay/Environment.js and add the following code. This is a basic Relay Environment that can handle GraphQL Queries - you'll add subscription support later. For more info on Relay environments see https://relay.dev/docs/en/relay-environment.html.

/* global fetch */
import {
  Environment,
  Network,
  RecordSource,
  Store
} from 'relay-runtime'

function fetchQuery (
  operation,
  variables
) {
  const headers = {
    'Content-Type': 'application/json'
  }
  // This url matches the example server by default - change it to match your environment if needed.
  return fetch('http://localhost:4001/graphql', {
    method: 'POST',
    headers,
    body: JSON.stringify({
      query: operation.text,
      variables
    })
  }).then(response => {
    return response.json()
  })
}

const environment = new Environment({
  network: Network.create(fetchQuery),
  store: new Store(new RecordSource())
})

export default environment

3. Building React Components and Fetching Initial Data

Now lets set up React components to display our messages. We know from looking at our server's schema that messages will have id, username, and message fields. Create a file at src/Message.js and add the following code to it:

// #1
import React, { Component } from 'react'
import { graphql } from 'babel-plugin-relay/macro'
import { createFragmentContainer } from 'react-relay'

// #2
class Message extends Component {
  render () {
    const { username, message } = this.props.message
    return (
      <li>
        {username}: {message}
      </li>
    )
  }
}

// #3
export default createFragmentContainer(
  Message,
  graphql`
    # As a convention, we name the fragment as '<ComponentFileName>_<propName>'
    fragment Message_message on newMessage {
      username
      message
    }
  `
)

Let's briefly review the code above.

  1. We import React and utilities for Relay. Note that graphql is imported from a babel plugin - this is what allows the Relay compiler to work its magic on GraphQL statements during compilation.

  2. We create a very basic React component that simply displays a username and message in a <li> element.

  3. We use a Relay Fragment Container to specify the data our component needs.

Now open src/App.js and replace its code with the following:

import React, { Component } from 'react'
import './App.css'
import { graphql } from 'babel-plugin-relay/macro'
import { QueryRenderer } from 'react-relay'
import environment from './Relay/Environment'
import Message from './Message'

class App extends Component {
  render () {
    return (
      <QueryRenderer
        environment={environment}
        query={graphql`
          query AppQuery{
            messages(last: 10) @connection(key: "AppQuery_messages") {
              edges{
                node{
                  id
                  ...Message_message
                }
              }
            }
          }
        `}
        variables={{}}
        render={({ error, props }) => {
          if (error) {
            console.error(error)
            return <div>Error!</div>
          }
          if (!props) {
            return <div>Loading...</div>
          }
          return <div>
            Messages:
            <ul>
              {props.messages.edges.map(edge => <Message key={edge.node.id} message={edge.node} />)}
            </ul>
          </div>
        }}
      />
    )
  }
}

export default App

In this component we're implementing Relay's QueryRenderer to fetch up to the last ten messages on the server and render them in a list using our simple Message component.

Note the @connection(key: "AppQuery_messages") annotation. This annotation is an identifier that will help us later when we want to display additional messages from a subscription - think of it as a tag that makes it easy to find a collection of GraphQL records in the Relay store.

This completes the basic React app displaying data with a GraphQL Query. Let's build the app and see it in action!

Run npm run relay to execute the Relay compiler, then run npm start to launch the app in a browser.

Important Note: the Relay compiler must be run any time you modify your GraphQL statements in your React app.

messages - demouser1: hello world; Hunter2: change your password

Now let's add new messages to our list by implementing a GraphQL subscription.

4. Implementing Subscriptions

If you look at the GraphQL schema you can see that the server offers one possible subscription, called 'messages', and it returns data of the same type as our earlier GraphQL query - a newMessageEdge. Let's implement a subscription to subscribe the client to any new messages sent by the server (the demo server automatically makes new messages every few seconds).

Install Additional Dependencies

First we need to install some additional dependencies that will help us connect Relay to our GraphQL server's websockets.

npm install --save apollo-link apollo-link-ws subscriptions-transport-ws

Update Relay Environment

Open src/Relay/Environment.js and import those dependencies at the top of the file:

import { execute } from 'apollo-link'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { WebSocketLink } from 'apollo-link-ws'

Next, update and add the following code at the end of the file, replacing the last ~6 lines of the file with the following code:

const subscriptionClient = new SubscriptionClient('ws://localhost:4001/graphql', {
  reconnect: true
})

const subscriptionLink = new WebSocketLink(subscriptionClient)

// Prepare network layer from apollo-link for graphql subscriptions
const networkSubscriptions = (operation, variables) =>
  execute(subscriptionLink, {
    query: operation.text,
    variables
  })

const environment = new Environment({
  network: Network.create(fetchQuery, networkSubscriptions),
  store: new Store(new RecordSource())
})

export default environment

You can see the full updated file here. This sets up a new websocket connection to the server and tells Relay's network layer how to use it.

Update App.js

Finally, we need to update src/App.js to set up our subscription when the component mounts, and remove it when the component unmounts.

Add the following import to src/App.js:

import { ConnectionHandler } from 'relay-runtime'

ConnectionHandler is the magic sauce that allows us to connect our newly received subscription data to existing data in the Relay store.

Add the following functions just after class App extends Component { in src/App.js:

componentDidMount () {
  const subscriptionConfig = {
    // #1
    subscription: graphql`
      subscription AppMessagesSubscription {
        messages {
          node {
            ...Message_message
            id
          }
        }
      }
    `,
    onCompleted: data => console.log(data),
    onError: error => console.error(error),
    // #2
    updater: store => {
      const newRecord = store.getRootField('messages').getLinkedRecord('node')
      // #2A
      const conn = ConnectionHandler.getConnection(
        store.getRoot(),
        'AppQuery_messages'
      )
      // #2B
      const edge = ConnectionHandler.createEdge(store, conn, newRecord, 'newMessageEdge')
      ConnectionHandler.insertEdgeAfter(conn, edge)
    }
  }

  this.subscription = requestSubscription(environment, subscriptionConfig)
}

componentWillUnmount () {
  this.subscription.dispose()
}

This is all that is necessary to implement our subscription. Let's talk through a few of the specific parts:

  1. subscription - This is our GraphQL subscription statement - note that we're still using Relay's composition here to determine which properties of a message we care about, using ...Message_message.

  2. updater - This function tells Relay how to update the store with the new data received from the subscription.

    • First we get our newly received record.
    • A. Next, we get the 'connection' used by our original GraphQL Query. The second argument to getConnection() is the key we previously set in the annotation in our QueryRenderer component.
    • B. We now tell Relay that this new data is an edge, and can be connected / inserted after the edges we got from our original query.

Let's re-compile our Relay app and start it to see subscriptions working.

npm run relay
npm start

screenshot of additional messages appearing from demo-user

Summary

The steps to making a Relay framework based React component subscription-aware are: configuring the network layer, setting up a subscription handler, tagging the connection where new data will be inserted in the store, and telling the store how to connect the new data to existing data using ConnectionHandler from relay-runtime. I hope this saves someone a few hours of digging through documentation.