During the development of a single page application, I had to ensure that the data shown to the user (frontend) was always the most up-to-date data from the app's database (backend), even if the user didn’t perform any action, such as reloading the page.

This way, a certain user could have the app opened on multiple devices, perform an action on one device that modifies the app's database, and the app will automatically sync across all his devices.

 

 

So, how can we do such a thing ? 

 

WebSockets to the rescue !

Socket.IO is a nodejs library that enables real-time bidirectional event-based communication.

We are going to install two packages:

  • socket.io, which is the websocket server that is going to notify clients of new data updates
  • socket.io-client, the client that will listen for 'data updates' events
npm i socket.io socket.io-client -S

 

The server side implementation is rather straightforward (note that we have dedicated rooms for each user) :

import * as SocketIO from 'socket.io';

class DataSyncRepository {
  protected io;

  constructor(httpServer) {
    this.io = SocketIO(httpServer);

    //this.io.use(this.authenticationMiddleware.bind(this));

    this.io.on('connection', (client) => {
      // Note: the authenticatedUser property is set by the authentication middleware
      if (client.request.authenticatedUser) {
        client.join(`user:${client.request.authenticatedUser.id}`);
      }
    });
  }

  notifyChange(userId, objectName, objectId) {
    if (this.io !== undefined) {
      this.io.to(`user:${userId}`).emit('data_change', {
        type: objectName,
        id: objectId
      });
    }
  }
}

 

Now, all we have to do is to call the notifyChange method right after the application's data is updated and an event will be propagated to the connected clients.

 

Client side data loading

In the first iteration of the app, the data needed by each React component was loaded in the componentWillMount lifecycle event through API calls.

It was working fine, the data was loaded every time the components were mounted, but we can do better.

 

Synchronizing React components

Following up on the previous server-side setup, we need to setup a websocket client that will listen for 'data updates' events, and who will propagate them to the appropriate React components through the Redux store.

 

 

First, we have to figure out where to initialize the websocket client. We need someplace that is executed only once - at the initialization of the React app - and from which we have access to the Redux store. The constructor of the root component (<App />) seems like a logical place to initialize the socket.io client.

We are going to use it to send and dispatch a DATA_CHANGED action to our app's redux store whenever we receive an event from the websocket server.

import * as SocketIOClient from 'socket.io-client';
import * as React from 'react';

class App extends React.Component {
  constructor(props) {
    super(props);

    if (this.io === undefined) {
      this.io = SocketIOClient.connect('http://localhost:3000');

      this.io.on('data_change', (data) => {
        const actionType = `DATA_CHANGED_${data.type.toUpperCase()}`;
        this.context['store'].dispatch({ type: actionType });
      });
    }
  }
}

App.contextTypes = {
  store: React.PropTypes.object.isRequired
};

export default App;

 

Then, our reducer's job is to set the state's dataIsDirty property to true, and it will be passed to the appropriate components (because they’re connect()ed to the store).

const reducer = function (state, action) {
  switch (action.type) {
    case 'DATA_CHANGED_LOCATIONS':
      return Object.assign({}, state, {
        dataIsDirty: true,
      });
    default:
      return Object.assign({}, state, {
        dataIsDirty: false,
      });
  }
};

 

The React components are subscribed to Redux store updates and will re-render as soon as the value of dataIsDirty is set to true.

import * as React from 'react';
import { connect } from 'react-redux';

class LocationList extends React.Component {
  public componentWillMount() {
    if (this.props.dataIsDirty === true) {
      // add the logic to fetch the data from the backend
    }
  }

  public componentWillUpdate() {
    if (this.props.dataIsDirty === true) {
      // add the logic to fetch the data from the backend
    }
  }
}

export default connect(
  state => state.locations,
)(LocationList);

 

That's it ! Everything is now in place ! The only thing left is to add the logic to fetch the data from the backend.