Realtime GraphQL UI Updates in React with Apollo.


Introduction

This is the fourth and final part of a 4-part series of articles explaining the implementation of GraphQL on an Express Server and similarly in a React client using Apollo. In this article we look at how to maintain realtime UI updates after implementing different GraphQL updates that result in either creation, update or deletion of data on the server side. For part 1, 2 and 3, refer to the links:

Updating the client state after a mutation

In the second part of this series of articles, we had to reload the page to see the new channel that had been created; there was no automatic re-rendering of the UI. This is because Apollo has no way of knowing that the mutation we made has anything to do with the channels query that renders our list. Only the server knows that, but it has no way of notifying our client. To update the client after a mutation, we can opt for on of the following three ways:

  • Refetch queries that could be affected by the mutations.
  • Manually updating the client state based on the mutation result.
  • Using GraphQL subscriptions.

Refetch

Refetches are the simplest way to force a portion of your cache to reflect the information available to your server. Essentially, a refetch forces a query to immediately hit the server again, bypassing the cache. The result of this query, just like all other query results, updates the information available in the cache, which updates all of the query results on the page. To tell Apollo Client that we want to refetch the channels after our mutation completes, we pass it via the refetchQueries option on the call to mutate. We will reuse the channelsListQuery by exporting it from ChannelsList.jsx and import it in CreateChannel.jsx.

//  src/app/components/ChannelList/ChannelList.jsx
export const channelsListQuery = gql`
   query ChannelsListQuery {
     channels {
       id
       name
     }
   }
 `;

We then pass the query via refetchQueries option in mutate.

...
// src/app/components/CreateChannel/CreateChannel.jsx
...
import { channelsListQuery } from '../ChannelList/ChannelList';
const CreateChannel = ({mutate}) => {
    const handleKeyUp = (evt) => {
      if (evt.keyCode === 13) {
        evt.persist();
        mutate({
          variables: { name: evt.target.value },
          refetchQueries: [ { query: channelsListQuery }]
        })
        .then( res => {
          evt.target.value = '';
        });
      }
  };
  ...

When we create a new channel right now, the UI shows it without reloading the page.

This is a good step but the downside is that this method will not show the updates if the request was made by another client. You won’t find out until you do a mutation of your own and refetch the list from the server or reload the page which we are trying to avoid. To make the application more realtime, Apollo provides a feature that allows us to poll queries within an interval. To enable this feature, we pass the pollInterval option with our channelsListQuery.

//  src/app/components/ChannelList/ChannelList.jsx
...
const ChannelsListWithData = graphql(channelsListQuery, { options: { pollInterval: 5000 }})(ChannelsList);
...

Creating a new channel from another client now updates our list of channels after 5 seconds, not the best way of doing it but a step in making the application realtime.

Updating the Store

To update the store based on a client action, Apollo provides a set of tools to perform imperative store updates: readQuery,writeQuery, readFragment and writeFragment. The client normalizes all of your data so that if any data you previously fetched from your GraphQL server is updated in a later data fetch from your server then your data will be updated with the latest truth from your server. Apollo exposes these functions via the update property in mutate. Using update gives you full control over the cache, allowing you to make changes to your data model in response to a mutation in any way you like.

To implement it, we replace the refetchQueries option with the following call to update.

// src/app/components/CreateChannel/CreateChannel.jsx
...
mutate({
          variables: { name: evt.target.value },
          update: (store, { data: { addChannel } }) => {
            // Read the data from the cache for this query.
            const data = store.readQuery({query: channelsListQuery });
            // Add our channel from the mutation to the end.
            data.channels.push(addChannel);
            // Write the data back to the cache.
            store.writeQuery({ query: channelsListQuery, data });
          }
        })
...

As soon as the mutation completes, we read the current result for the channelsListQuery out of the store, append the new channel to it, and tell Apollo Client to write it back to the store. Our ChannelsList component will automatically get updated with the new data. There’s still a delay which is a result of the network request. Apollo provides, optimistic UI, a feature that allows your client code to easily predict the result of a successful mutation even before the server responds with the result providing a fater user experience. This is possible with Apollo if the client can predict an optimistic response for the mutation.

Optimistic UI

To enable optimistic UI, we add the optimisticResponse property to mutate call. This “fake result” will be used to update active queries immediately, in the same way that the server’s mutation response would have done. The optimistic patches are stored in a separate place in the cache, so once the actual mutation returns, the relevant optimistic update is automatically thrown away and replaced with the real result. Since the server generates the channel Id’s which we use as keys in rendering the list, we will generate random keys to use before the server response and retain the channel name since we know what we expect. Finally, we also have to specify the __typename to make sure Apollo Client knows what kind of object it is.

// src/app/components/CreateChannel/CreateChannel.jsx
...
       variables: { name: evt.target.value },
          optimisticResponse: {
             addChannel: {
               name: evt.target.value,
               id: Math.round(Math.random() * -1000000), // Generate a random ID to avoid conflicts.
               __typename: 'Channel',
             },
          },
          update:
...

After this addition, creating a new channel instantly shows the channel on the list with no delays. These updates have not been confirmed by the server yet. For purposes of development and knowledge, we can choose to show the user which items are pending confirmation. As you can notice, we used negative values for the random Id’s; this enables us to differentiate already confirmed channels which have positive Id’s. We could add a CSS class to highlight channels awaiting confirmation.

 ...
 return <ul className="list-group">
     { channels.map( ch => <li className={ "list-group-item " + (ch.id < 0 ? "pending" : "")} key={ch.id}>{ch.name}</li> ) }
   </ul>;
...

For a brief moment, with the addition of pending class, the new item appears red before turning to black. We can make this behaviour more defined so as to observe it for development pusrposes by simulating a latency in the network. In the following snippet, we fake a 3-second network delay that allows us to see the UI changes clearly. We use applyMiddleware which allows us to modify the request made over the netWorkInterface.

// src/app/app.jsx
...
networkInterface.use([{
  applyMiddleware(req, next) {
    setTimeout(next, 3000);
  },
}]);
...

Initially.

After 3 seconds.

Subscriptions

Use of GraphQL subscriptions is a way to push data from the server to the clients that choose to listen to real time messages from the server. Subscriptions are similar to queries in that they specify a set of fields to be delivered to the client, but instead of immediately returning a single answer, a result is sent every time a particular event happens on the server.

A common use case for subscriptions is notifying the client side about particular events, for example the creation of a new object, updated fields and so on. This section relies on server-side implementation of subscriptions and I would advise going through it for maximum gain. The first step to adding subscriptions to our client is to set up the WebSocket connection that the client and server will communicate over. Forming and maintaining the WebSocket connection will be the job of the Apollo network interface, defined in client/src/App.js. To add WebSocket support to our existing interface, we will construct a GraphQL Subscription client and merge it with our existing network interface to create a new interface that performs normal GraphQL queries over HTTP and subscription queries over WebSockets. The most popular transport for GraphQL subscriptions today is subscriptions-transport-ws which we can install using:

# Note the package version. Future updates will change the implementation.
yarn add subscriptions-transport-ws@0.8.2   # Alternatively npm i subscriptions-transport-ws@0.8.2

We also remove the following part used to demonstrate refetching of queries and polling to show the real power of real-time updates.

//src/app/components/CreateChannel/CreateChannel.jsx
...
 refetchQueries: [ { query: channelsListQuery }]  // Remove this part to disable query refetching feature.
...

We edit the following part to disable polling.

//src/app/components/ChannelList/ChannelList.jsx
...
 const ChannelsListWithData = graphql(channelsListQuery, { options: { pollInterval: 5000 }})(ChannelsList);
...

Edit the above to:

//src/app/components/ChannelList/ChannelList.jsx
...
 const ChannelsListWithData = graphql(channelsListQuery)(ChannelsList);
...

Trying to add a new channel now in our app does not reflect the changes until we refresh the page and that is what we expect.

Then, initialize a GraphQL subscriptions transport client and merge it with our existing network interface.

 //src/app/app.jsx
...
import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws';
...
const networkInterface = createNetworkInterface({
  uri: 'http://localhost:7900/graphql',
});

const wsClient = new SubscriptionClient(`ws://localhost:7900/subscriptions`, {
  reconnect: true
});

// Extend the network interface with the WebSocket
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
  networkInterface,
  wsClient
);

To enable subscriptions throughout our application, we use networkInterfaceWithSubscriptions as the Apollo Client’s network interface:

const client = new ApolloClient({
   networkInterface: networkInterfaceWithSubscriptions // Use the subscriptions interface as the client interface.
});

When we inspect our network in the browser, it shows that we are subscribed to a channel.

Our queries and mutations will now go over HTTP as normal, but subscriptions will be done over the WebSocket transport.

Listening for Messages

Now that we can use GraphQL Subscriptions in our client, the next step is to use subscriptions to detect the creation of new channels. Our goal here is to use subscriptions to update our React views to see new channels as they are added. We begin by writing the subscription.

// src/app/components/ChannelList/ChannelList.jsx
...
// Create a subscription
const channelSubscription = gql`
    subscription Channels {
     channelAdded {
       id
       name
     }
    }
`
...

With GraphQL subscriptions your client will be alerted on push from the server and we can use the data sent along with the notification and merge it directly into the store (existing queries are automatically notified). We can accomplish this using subscribeToMore function available on every query result in react-apollo. The update function gets called every time the subscription returns.

Before we start, we have to refactor our client/src/components/ChannelDetails.js component to be a full ES6 class component instead of just a function, so that we can use the React lifecycle events to set up the subscription.

In componentWillMount we add a functionaliity that will subscribe using subscribeToMore and update the query’s store with the new data using updateQuery. The updateQuery callback must return an object of the same shape as the initial query data, otherwise the new data won’t be merged.

//src/app/components/ChannelList/ChannelList.jsx
componentWillMount() {
   this.props.data.subscribeToMore({
     document: channelSubscription,   // Use the subscription
     updateQuery: (prev, {subscriptionData}) => {
       if (!subscriptionData.data) {
         return prev;
       }

       const newChannel = subscriptionData.data.channelAdded;
       // Add check to prevent double adding of channels.
      if (!prev.channels.find((channel) => channel.name === newChannel.name)) {
         let updatedChannels = Object.assign({}, prev, { channels : [...prev.channels, newChannel ] });
         return updatedChannels;
       } else {
         return prev;
       }
     }
   });
 }
 render() {
 ...

Testing

We can now test our application by opening two browser windows at http://localhost:7800/ side by side. Adding a channel in either window immediately shows the new channel in the adjacent window.

Conclusion

We have explored different ways in which we can update our client to avoid page refreshes and large data requests and we can conclude that using subscriptions is the fastest and most efficient way of achieving this.

This content was originally published here.

Other FinTech Healines