Redux WebSocket Integration

This article aims to provide a concise summary of the steps taken to implement WebSocket functionality into a React/Redux application architecture. It assumes a basic level of knowledge around Redux state management.


Our Starting State

Developing a clear understanding of the Redux data flow model can take time, even for experienced developers. A quick Google image search for say “Redux store architecture” will reveal a large and varied set of diagrams attempting to visualise the process; lots are overly simplistic, while many examples are shrouded in their own complexity.

To understand how WebSockets fit within our application architecture, I will provide a visual representation of the Redux model before and after WebSocket support has been added.

The clearest flow diagram I have come across is from this excellent introductory article on Redux by David Geary.

If you are new to Redux, I’d certainly recommend taking the time to read this article before continuing here.

Back to the visualisation…

Fig.1 Redux state management process flow

In summary, the user interacts with the view, which dispatches an action. This action, along with the current state are passed to the reducer to generate a new state in the store. Any view components are in turn updated to reflect this new state.

So how would WebSockets fit into this architecture? The following sections discuss the steps taken to implement WebSocket support. An updated diagram will follow to provide visual clarity of where this functionality resides within our Redux model.


Why Integrate WebSockets Into Redux?

When I first started thinking about leveraging the power of WebSockets, I made the mistake of thinking outside of Redux - about wiring up event handlers within the view components that would trigger the request. I did however foresee issues with this approach — it would potentially be difficult to keep the state in sync across multiple devices (since WebSockets would be operating outside the store) and it wouldn’t be particularly scalable (every message/action type we wanted to support would require their own event handlers to be added for example).

Whilst performing further research on WebSocket integrations, I came across this excellent Git repo from Max Nachlinger, which provided the basis of what I was looking for — WebSocket integration within the Redux store itself.

This repo uses the tried and tested socket.io library that “… enables real-time bidirectional event-based communication”. Socket.io offers wide platform and device support and performs a significant amount of the heavy lifting for you; great for prototyping, freeing me up to focus on integrating with Redux.

Socket.io consists of two different modules, socket.io which is the server module and the front end library modulesocket.io-client.

The setup and configuration of the server isn’t covered by this article, but this article from Mark Brown was very helpful in allowing me to quickly bring up a test WebSocket server.

With a server up and running, it was over to the Redux integration. Firstly, I needed to add the SocketIO client module as a dependency yarn add socket.io-client .

Remember WebSockets are bidirectional? This meant I had to implement a method for sending messages and a process for setting up listeners for each WebSocket supported message type.


Send/Receive

The core WebSocket functionality for my app is held in a single actions/websockets.js file. This is a very lightweight file that imports the socket.io-client module and a constants file.

The URI from the constants file is used to configure the socket. Messages will be sent to this server and sockets will listen for messages from this socket.

It exports a couple of very simple methods; one for sending messages and one initialising listeners.

import io from 'socket.io-client';
import {messageTypes, uri} from '../constants/websocket.js';
const socket = io( uri );
export const init = ( store ) => {
  Object.keys( messageTypes )
    .forEach( type => socket.on( type, ( payload ) => 
store.dispatch({ type, payload })
)
);
};
export const emit = ( type, payload ) => socket.emit( type, payload );

The emit() method is a simple one liner. It takes the message type as the first parameter and the payload as the second parameter. These values are then relayed to the server using the socket.emit() method.

Setting up the listeners is only slightly more complicated. The init() method takes the Redux store as its single parameter. The function will then loop through the list of WebSocket supported message types — these are configured within the constants file:

...
const messageTypes = [UPDATE_STAGE_TITLE].reduce( ( accum, msg ) =>
{
accum[ msg ] = msg;
return accum;
}, {}
);
...

In this example, only one action is supported — “UPDATE_STAGE_TITLE”. For each message it finds, a listener will be created socket.on( type, ( payload )… . When the socket receives a message of a supported type, that action will be dispatched by the store with its payload. As with any dispatch, a new state will be returned and any dependent view components updated accordingly.

Configure Store

Now we have our send and receive methods, we need to setup their invocation. Both are attached through store configuration.

My application contains its own file dedicated to configuring the store store/configure-store.js , so I updated this as follows.

I was already applying middleware for various tasks, so I just injected the emit() method as a custom argument on my Thunk Middleware.

const middleware = [ thunkMiddleware.withExtraArgument({ emit }) ];

This allows us to call the emit() function when dispatching actions later on.

Once the store has been created, it can be passed to the WebSocket init() function defined earlier, which will instantiate all the required listeners.

...
const store = createStore(
reducer,
preloadedState,
compose(
applyMiddleware(
...middleware,
loggerMiddleware
),
offline( offlineConfig )
)
);
websocketInit( store );
return store;
...

Invoke the emit() method

Finally, we need to call the emit() method to trigger a WebSocket request when our supported actions are dispatched.

For example, in my ToDo application a user can change the title of a stage, which will trigger a WebSocket message to be sent to the server. This change will then be pushed out to all other subscribers, the action will be dispatched and the view components updated to reflect the change.

In order to trigger the method, I added an onBlur event to my stage class (React Container):

onBlur( event ) {
const initVal = this.state.initVal;
if ( initVal !== event.target.value ) {
const { dispatch } = this.props;
dispatch( wsStageTitle( this.props.stage.stageId, event.target.value ) );
this.setState({initVal: this.props.stage.name});
}
}

If the new value differs from the initial value this method dispatches a wsStageTitle function, which has the stageID and the new title passed to it.

This function is defined in my action/stages.js action file:

export function wsStageTitle( stageId, stageTitle ) {
return ( dispatch, getState, {emit}) => {
dispatch({
type: UPDATE_STAGE_TITLE,
payload: {
stageId: stageId,
stageTitle: stageTitle
}
}),
emit( messageTypes.UPDATE_STAGE_TITLE, { stageId: stageId, stageTitle: stageTitle });
};
}

The “UPDATE_STAGE_TITLE” action is dispatched on the store, followed by the additional invocation of the emit() function. This is available to us, as it was attached to the store earlier.

Whenever the app is required to send an update via the WebSocket, we can call this emit() function.

Our new architecture

In order to implement this WebSocket architecture we’ve touched on a few areas of the code. For additional clarity, the diagram below illustrates where these changes fit within our Redux model from earlier.

Fig.2 Redux state management with WebSocket integration

Conclusion

WebSockets and Redux are a great fit. Through following the process outlined in this article, I was able to implement a simple, configurable, highly scalable solution which sees WebSocket communication tightly coupled to the core Redux state management flow. As such, we have avoided potential difficulties with keeping multiple user states in sync. The scalable nature of the solution means it’s simple to extend support to new action types moving forward.


Checkout my next article which covers adding offline support for Redux to this application architecture:

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.