Permalink
Fetching contributors…
Cannot retrieve contributors at this time
300 lines (224 sloc) 11.5 KB
title pageTitle description question answers correctAnswer videoId duration videoAuthor
Realtime Updates with GraphQL Subscriptions
Realtime with GraphQL Subscriptions, React & Apollo Tutorial
Learn how to implement realtime functionality using GraphQL subscriptions with Apollo Client & React. The websockets will be handled by subscriptions-transport-ws.
What transport does Apollo use to implement subscriptions?
WebSockets
TCP
UDP
HTTP 2
0
0

This section is all about bringing realtime functionality into the app by using GraphQL subscriptions.

What are GraphQL Subscriptions?

Subscriptions are a GraphQL feature allowing the server to send data to its clients when a specific event happens. Subscriptions are usually implemented with WebSockets, where the server holds a steady connection to the client. This means when working with subscriptions, you're breaking the Request-Response-Cycle that was used for all previous interactions with the API. The client now initiates a steady connection with the server by specifying which event it is interested in. Every time this particular event then happens, the server uses the connection to push the expected data to the client.

Subscriptions with Apollo

When using Apollo, you need to configure your ApolloClient with information about the subscriptions endpoint. This is done by adding another ApolloLink to the Apollo middleware chain. This time, it's the WebSocketLink from the apollo-link-ws package.

Go and add this dependency to your app first.

Open a terminal and navigate to the project's root directory. Then execute the following command:

yarn add apollo-link-ws

Note: For apollo-link-ws to work you also need to install subscriptions-transport-ws

yarn add subscriptions-transport-ws

Next, make sure your ApolloClient instance knows about the subscription server.

Open index.js and add the following import to the top of the file:

import { split } from 'apollo-link'
import { WebSocketLink } from 'apollo-link-ws'
import { getMainDefinition } from 'apollo-utilities'

Notice that you're now also importing the split function from 'apollo-link'.

Now create a new WebSocketLink that represents the WebSocket connection. Use split for proper "routing" of the requests and update the constructor call of ApolloClient like so:

const wsLink = new WebSocketLink({
  uri: `ws://localhost:4000`,
  options: {
    reconnect: true,
    connectionParams: {
      authToken: localStorage.getItem(AUTH_TOKEN),
    }
  }
})

const link = split(
  ({ query }) => {
    const { kind, operation } = getMainDefinition(query)
    return kind === 'OperationDefinition' && operation === 'subscription'
  },
  wsLink,
  authLink.concat(httpLink)
)

const client = new ApolloClient({
  link,
  cache: new InMemoryCache()
})

You're instantiating a WebSocketLink that knows the subscriptions endpoint. The subscriptions endpoint in this case is similar to the HTTP endpoint, except that it uses the ws instead of http protocol. Notice that you're also authenticating the websocket connection with the user's token that you retrieve from localStorage.

split is used to "route" a request to a specific middleware link. It takes three arguments, the first one is a test function which returns a boolean. The remaining two arguments are again of type ApolloLink. If test returns true, the request will be forwarded to the link passed as the second argument. If false, to the third one.

In your case, the test function is checking whether the requested operation is a subscription. If this is the case, it will be forwarded to the wsLink, otherwise (if it's a query or mutation), the authLink.concat(httpLink) will take care of it:

Picture taken from Apollo Link: The modular GraphQL network stack by Evans Hauser

Subscribing to new links

For the app to update in realtime when new links are created, you need to subscribe to events that are happening on the Link type. There generally are three kinds of events you can subscribe to when using Prisma:

  • a new Link is created
  • an existing Link is updated
  • an existing Link is deleted

You'll implement the subscription in the LinkList component since that's where all the links are rendered.

Open LinkList.js and update current component as follow:

class LinkList extends Component {
  _updateCacheAfterVote = (store, createVote, linkId) => {
    const data = store.readQuery({ query: FEED_QUERY })
  
    const votedLink = data.feed.links.find(link => link.id === linkId)
    votedLink.votes = createVote.link.votes
  
    store.writeQuery({ query: FEED_QUERY, data })
  }

  _subscribeToNewLinks = async () => {
    // ... you'll implement this 🔜
  }

  render() {
    return (
      <Query query={FEED_QUERY}>
        {({ loading, error, data, subscribeToMore }) => {
          if (loading) return <div>Fetching</div>
          if (error) return <div>Error</div>

          this._subscribeToNewLinks(subscribeToMore)
    
          const linksToRender = data.feed.links
    
          return (
            <div>
              {linksToRender.map((link, index) => (
                <Link
                  key={link.id}
                  link={link}
                  index={index}
                  updateStoreAfterVote={this._updateCacheAfterVote}
                />
              ))}
            </div>
          )
        }}
      </Query>
    )
  }
}

Let's understand what's going on here! You're using the <Query /> component as always but now you're using subscribeToMore received as prop into the component’s render prop function. Calling _subscribeToNewLinks with it respective subscribeToMore function you make sure that the component actually subscribes to the events. This call opens up a websocket connection to the subscription server.

Still in LinkList.js implement _subscribeToNewLinks like so:

_subscribeToNewLinks = subscribeToMore => {
  subscribeToMore({
    document: NEW_LINKS_SUBSCRIPTION,
    updateQuery: (prev, { subscriptionData }) => {
      if (!subscriptionData.data) return prev
      const newLink = subscriptionData.data.newLink.node

      return Object.assign({}, prev, {
        feed: {
          links: [newLink, ...prev.feed.links],
          count: prev.feed.links.length + 1,
          __typename: prev.feed.__typename
        }
      })
    }
  })
}

You're passing two arguments to subscribeToMore:

  1. document: This represents the subscription query itself. In your case, the subscription will fire every time a new link is created.
  2. updateQuery: Similar to cache update prop, this function allows you to determine how the store should be updated with the information that was sent by the server after the event occurred. In fact, it follows exactly the same principle as a Redux reducer: It takes as arguments the previous state (of the query that subscribeToMore was called on) and the subscription data that's sent by the server. You can then determine how to merge the subscription data into the existing state and return the updated data. All you're doing inside updateQuery is retrieve the new link from the received subscriptionData, merge it into the existing list of links and return the result of this operation.

The last thing you need to do for this to work is add the NEW_LINKS_SUBSCRIPTION to the top of the file:

const NEW_LINKS_SUBSCRIPTION = gql`
  subscription {
    newLink {
      node {
        id
        url
        description
        createdAt
        postedBy {
          id
          name
        }
        votes {
          id
          user {
            id
          }
        }
      }
    }
  }
`

Awesome, that's it! You can test your implementation by opening two browser windows. In the first window, you have your application running on http://localhost:3000/. The second window you use to open a Playground and send a post mutation. When you're sending the mutation, you'll see the app update in realtime! ⚡️

ATTENTION: There's a currently a bug in the apollo-link-ws package that will prevent your app from running due to the following error: Module not found: Can't resolve 'subscriptions-transport-ws' in '/.../hackernews-react-apollo/node_modules/apollo-link-ws/lib' The workaround until it's fixed is to manually install the subscriptions-transport-ws package with yarn add subscriptions-transport-ws. There's also another bug in graphql-yoga which causes the subscription to fire multiple times (while the link is in fact only created once). After reloading the site, you'll see the correct number of links.

Subscribing to new votes

Next you'll subscribe to new votes that are submitted by other users so that the latest vote count is always visible in the app.

Open LinkList.js and add the following method to the LinkList component:

_subscribeToNewVotes = subscribeToMore => {
  subscribeToMore({
    document: NEW_VOTES_SUBSCRIPTION
  })
}

Similar as before, you're calling subscribeToMore but now using NEW_VOTES_SUBSCRIPTION as document. This time you're passing in a subscription that asks for newly created votes. When the subscription fires, Apollo Client automatically updates the link that was voted on.

Still in LinkList.js add the NEW_VOTES_SUBSCRIPTION to the top of the file:

const NEW_VOTES_SUBSCRIPTION = gql`
  subscription {
    newVote {
      node {
        id
        link {
          id
          url
          description
          createdAt
          postedBy {
            id
            name
          }
          votes {
            id
            user {
              id
            }
          }
        }
        user {
          id
        }
      }
    }
  }
`

Finally, go ahead and call _subscribeToNewVotes inside render as you did with _subscribeToNewLinks:

this._subscribeToNewLinks(subscribeToMore)
this._subscribeToNewVotes(subscribeToMore)

Fantastic! Your app is now ready for realtime and will immediately update links and votes whenever they're created by other users.