GraphQL

GraphQL Apollo Subscriptions for Realtime Updates in React

February 2, 2018

author:

GraphQL Apollo Subscriptions for Realtime Updates in React

GraphQL Subscriptions are used to push data from server to client. The client can subscribe to events to listen to an update as and when data in the server changes. And therefore, first of all, we need to implement subscriptions on the server side to publish events and thus notify the client regarding updates. We already did this in the last tutorial. In this section, we will look after the UI updates in our react application.

# How to Autorefresh React App with GraphQL?

We have already built a react app and components for CRUD operation using GraphQL API. The problem with this application, for now, is that user needs to manually refresh the browser to use any CRUD changes. This affects UX to a great extent. We will take forward the same app forward and wire subscriptions to it so that user can see updates right away without any intervention.

Add Buttons in React App

# WebSocket Setup

First of all, we will install a third-party library that can help us wire apollo subscriptions and web socket:

npm install subscriptions-transport-ws@0.8.2 --save

To get realtime updates from the server, we need a socket connection between client and server for faster communication. That is, subscription queries must run on a web socket while the normal queries and mutations can run over HTTP.

# WebSocket Implementation

Let us now wire the installed library within our root component app.js:

# src/app/app.js
...
import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws';

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

Here we are importing and initializing the client from GraphQL subscriptions transport. We also mention the URI link for WebSocket subscriptions.

const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
 networkInterface,
 wsClient
);

Next, we are wiring the newtworkInterface (for HTTP Req) and wsClient (for WS Req) together.

# src/app/app.js
...
//remove
networkInterface.use([{
 applyMiddleware(req, next) {
  setTimeout(next, 1000);
 },
}]);

We remove the middleware since we don’t really need to implement the middleware separately for HTTP endpoint.

# src/app/app.js
...
//add
const client = new ApolloClient({
 networkInterface : networkInterfaceWithSubscriptions
});

And now, clients can utilize both the endpoints that we declared in networkInterfaceWithSubscriptions in the whole application.

Here’s our final app.js:

# src/app/app.js

import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { ApolloClient, ApolloProvider, createNetworkInterface } from 'react-apollo';
import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws';

import CompaniesList from './components/list_company';
import CreateCompany from './components/create_company';
import DisplayCompany from './components/display_company';
import EditCompany from './components/update_company';
import '../style/app.scss';

const networkInterface = createNetworkInterface({
  uri: 'http://localhost:7700/graphql',
});

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

const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
  networkInterface,
  wsClient
);

const client = new ApolloClient({
  networkInterface : networkInterfaceWithSubscriptions
});

let app = document.querySelector('#app');

render(
<ApolloProvider client={client}>
  <div className="App">
    <div className="row">
      <div className="col-lg-6 col-lg-offset-3">
        <BrowserRouter>
          <div>
            <Switch>
              <Route path="/company/create" component={CreateCompany} />
              <Route path="/company/:cId/update" component={EditCompany} />
              <Route path="/company/:postId" component={DisplayCompany} />
              <Route path="/" component={CompaniesList} />
            </Switch>
          </div>
        </BrowserRouter>
      </div>
    </div>
  </div>
</ApolloProvider>,
app
)

We can check this in a browser by inspecting the network elements:

GraphQL subscription in browser network tab

# GraphQL Subscriptions

Let us now define the subscriptions so that the react app receive notifications and updated data. We will use subscriptions to listen to changes caused during insert, update and delete operation:

Insert:

const companyAddSub = gql`
   subscription companyAdded {
    companyAdded {
       id
       name
    }
   }
`;

Update:

const companyEditSub = gql`
  subscription companyEdited {
   companyEdited {
     id
     name
   }
  }
`;

Delete:

const companyDeleteSub = gql`
  subscription companyDeleted {
   companyDeleted {
     id
     name
   }
  }
`;
companyAdded, companyEdited and companyDeleted come from our server specifications in the schema.js file.

# Update React UI for GraphQL API

The react component automatically re-renders itself when any data changes. Thus all we have to concentrate on is to tweak the data when any subscription event occurs. Also, here’s the complete list_company component for reference. Don’t feel overwhelmed; I’ll walk you through each component componentWillReceiveProps in a moment as we have already discussed the rest of the part.

# src/app/components/list_company.js
import _ from 'lodash';
import React, { Component } from 'react';
import { gql, graphql, compose } from 'react-apollo';
import { Link } from "react-router-dom";
class CompaniesList extends Component {
componentWillReceiveProps(nextProps) {
if (!this.subscription && !nextProps.data.loading) {
let { subscribeToMore } = this.props.data
this.subscription = [subscribeToMore(
{
document: companyAddSub,
updateQuery: (previousResult, { subscriptionData }) => {
const newCo = subscriptionData.data.companyAdded;
if (!previousResult.Companies.find((c) => c.id === newCo.id)) {
let updatedList = Object.assign({}, previousResult, { Companies :[...previousResult.Companies, newCo ] });
return updatedList;
} else {
return previousResult;
}
},
}),
subscribeToMore(
{
document: companyEditSub,
updateQuery: (previousResult, { subscriptionData }) => {
const editCo = subscriptionData.data.companyEdited;
if (previousResult.Companies.find((c) => c.id === editCo.id)) {
let updatedList = Object.assign({}, previousResult, { Companies :[...previousResult.Companies ] });
return updatedList;
} else {
return previousResult;
}
},
}),
subscribeToMore(
{
document: companyDeleteSub,
updateQuery: (previousResult, { subscriptionData }) => {
const delCo = subscriptionData.data.companyDeleted;
const oldList = previousResult.Companies;
let newList = oldList.filter(function (elem) {
return elem.id !== delCo.id
});
let updatedList = Object.assign({}, previousResult, { Companies: newList });
return updatedList;
},
})
]}
}
handleDelete (cId)  {
this.props.DeleteCo({variables: { id: cId }})
.then(console.log('deleted'));
}
render() {
const { loading, error, Companies }= this.props.data;
if (loading) {
return <p> Loading... </p>;
}
if (error) {
return <p> { error.message } </p>;
}
return (
<div>
<div className="text-right">
<Link className="btn btn-primary" to="/company/create">
Add a Company
</Link>
</div>
<ul className = "list-group" > {
Companies.map(c => <li className = "list-group-item" key = { c.id }>
<div className="company-list">
<div className="company-name">
<Link to = {`/company/${c.id}`}>
{c.name}
</Link>
</div>
<div className="company-btn">
<Link className="btn btn-primary" to={`company/${c.id}/update`}>
Update
</Link>
<button type="submit" className="btn"
onClick={() => this.handleDelete(c.id)}>Delete</button>
</div>
</div>
</li>
) }
</ul>
</div>
);
};
}
static fragments = {
pokemon: gql`
fragment PokemonCardPokemon on Pokemon {
url
name
}
`
}
const CompanyListQuery = gql `
query CompanyList{
Companies {
id
name
}
}
`;
const DeleteCo = gql`
mutation deleteCompany($id: ID!) {
deleteCompany(id: $id) {
id
name
}
}
`;
const companyAddSub = gql`
subscription companyAdded {
companyAdded {
id
name
}
}
`;
const companyEditSub = gql`
subscription companyEdited {
companyEdited {
id
name
}
}
`;
const companyDeleteSub = gql`
subscription companyDeleted {
companyDeleted {
id
name
}
}
`;
const CompanyListWithMutations =  compose(
graphql(CompanyListQuery),
graphql(DeleteCo, {
name: 'DeleteCo'
})
)(CompaniesList)
export default CompanyListWithMutations;

UI Updates for Insert Operation Subscription Event:

We will do this in react’s life cycle componentWillReceiveProps method. The subscriptions-transport-ws library sends the updated data in data.subscribeToMore props. Let us scaffold the method:

componentWillReceiveProps(nextProps) {
if (!this.subscription && !nextProps.data.loading) {
let { subscribeToMore } = this.props.data
this.subscription = [subscribeToMore(
{
document: companyAddSub,
updateQuery: (previousResult, { subscriptionData }) => {
const newCo = subscriptionData.data.companyAdded;
//more logic
}
})
]
}
}

In the above code snippet, we are first validating the subscription for new updates and loading prop. Next, using es6, we are de-structuring the subscribeToMore prop from this.props.data and passing arguments to it. document depicts the subscription name we intend to listen to. We declared this in the previous step. updateQuery is used to update the data store. previousResult refers to the old/current stored data while subscriptionData is the new data pushed by the server for subscribers.

# src/app/components/list_company.js
...
class CompaniesList extends Component {
componentWillReceiveProps(nextProps) {
if (!this.subscription && !nextProps.data.loading) {
let { subscribeToMore } = this.props.data
this.subscription = [subscribeToMore(
{
document: companyAddSub,
updateQuery: (previousResult, { subscriptionData }) => {
const newCo = subscriptionData.data.companyAdded;
if (!previousResult.Companies.find((c) => c.id === newCo.id)) {
let updatedList = Object.assign({}, previousResult, { Companies :[...previousResult.Companies, newCo ] });
return updatedList;
} else {
return previousResult;
}
}
})
]
}
}
}

As a precaution, we first check whether the subscription has any data payload. subscriptionData.data.newPro has the fresh data from the server . (Note – newPro is the subscription name described in schema)

Since both the old store and fresh data set are read-only, we return a fresh object and append the new dataset that user inserted after checking for duplicate values with id.

Testing:

You can now insert a new company and see the instant updates:

http://localhost:8100
GraphQL Realtime Insert in React

UI Updates for Update Operation Subscription Event:

Since we will work on companyEditSub subscription, we will mention it as a document key.

subscribeToMore(
{
document: companyEditSub,
updateQuery: (previousResult, { subscriptionData }) => {
const editCo = subscriptionData.data.companyEdited;
if (previousResult.Companies.find((c) => c.id === editCo.id)) {
let updatedList = Object.assign({}, previousResult, { Companies :[...previousResult.Companies ] });
return updatedList;
} else {
return previousResult;
}
},
}),

Here we are checking for an opposite condition than insert event. We are finding the correct id from the old data store and returning a fresh object that has the updated data.

Testing:

You can now update an existing product and see the instant updates:

http://localhost:8100

UI Updates for Delete Operation Subscription Event:

We will begin with mentioning the document name we intend to work with and that is proDelete.

subscribeToMore(
{
document: companyDeleteSub,
updateQuery: (previousResult, { subscriptionData }) => {
const delCo = subscriptionData.data.companyDeleted;
const oldList = previousResult.Companies;
let newList = oldList.filter(function (elem) {
return elem.id !== delCo.id
});
let updatedList = Object.assign({}, previousResult, { Companies: newList });
return updatedList;
},
})

Here we are first removing the deleted company from the old data store and passing it to the fresh object.

Testing:

You can now delete an existing product and see the instant updates:

http://localhost:8100
GraphQL Realtime Delete in React

Conclusion:

GraphQL subscriptions enable realtime updates in the application. We utilized them in our react application to let users browse the application smoothly. Our client (react application) subscribed to the relevant events for this purpose and were notified via a web socket connection. We used third party library to configure the whole setup and then worked around wiring them in the components. We also tested the app for realtime updates and thus completed the UI cycle.

Questions or Comments:

Some gross stuff, isn’t it? If you face any issue in implementing GraphQL Apollo Subscriptions in React Application, comment them below.

Leave a comment

Your email address will not be published. Required fields are marked *