WebSockets in Redux: Write Your Own Middleware

Writing our own socket middleware for Redux.

By Ron Gierlach

Using Redux to manage state in your React application is great, but sifting through best patterns for handling async actions can sometimes have you questioning your design choices. I'd like to put forward two approaches to handling WebSockets that I've found useful with Redux.

The first approach, and the one I will cover in this post, is to write your own middleware.

Sound scary? It isn't! Check it out:

// First let's write some action types
const types = {
  SOCKET_OPEN: 'SOCKET_OPEN',
  SOCKET_CLOSE: 'SOCKET_CLOSE',
  SOCKET_ERROR: 'SOCKET_ERROR',
  SOCKET_MESSAGE: 'SOCKET_MESSAGE',
  SOCKET_CONNECT: 'SOCKET_CONNECT'
}

// Next let's write some action creators for handling socket activity
const actionCreators = {
  socketOpen: e => ({ type: types.SOCKET_OPEN }),
  socketClose: e => ({ type: types.SOCKET_CLOSE }),
  socketError: err => ({ type: types.SOCKET_ERROR, payload: err }),
  socketMessage: e => ({ type: types.SOCKET_MESSAGE, payload: JSON.parse(e.data) }),
  socketConnect: e => ({ type: types.SOCKET_CONNECT })
}

With me so far? Alright well now it's time to write the middleware itself:

import { bindActionCreators } from 'redux'

// Here we write the function for creating our middleware
// Let's break down these arguments...
const createSocketMiddleware = (
  socketURL, // the url our socket connects to
  subscribeData, // the handshake data our socket will send once connected (optional)
  shouldInstantiate, // a predicate function to know when to connect our socket
  eventHandlers // the actions we want our socket to dispatch
) => store => next => action => {
  if (shouldInstantiate(action)) {
    // instantiate the web socket
    const ws = new window.WebSocket(socketURl)
    // bind eventHandlers to dispatch
    const boundEventHandlers = bindActionCreators(eventHandlers, store.dispatch)
    // fire onopen event, and fire off a subscribe message with our handshake data
    ws.onopen = e => {
      boundEventHandlers.onopen(e)
      ws.send(JSON.stringify({ type: 'subscribe', ...subscribeData }))
    }
    // assign remaining event handlers
    ws.onclose = boundEventHandlers.onclose
    ws.onerror = boundEventHandlers.onerror
    ws.onmessage = boundEventHandlers.onmessage
  } else {
    return next(action)
  }
}

Finally we'll create a middleware instance and add it to our store:

import { createStore, applyMiddleware } from 'redux'

/* 
  ...our previous code, an initialState, and a rootReducer...
*/

const mySocketURL = 'wss://ws-feed.a-website.com'
const mySubscribeData = { rotationalAxisIds: ['x', 'y', 'z'] }
const mySocketPredicate = action => action.type === types.SOCKET_CONNECT
const myEventHandlers = {
  onopen: actionCreators.socketOpen,
  onclose: actionCreators.socketClose,
  onerror: actionCreators.socketError,
  onmessage: actionCreators.socketMessage
}

const mySocketMiddleware = createSocketMiddleware(
  mySocketURL,
  mySubscribeData,
  mySocketPredicate,
  myEventHandlers
)

const store = createStore(rootReducer, initialState, applyMiddleware(mySocketMiddleware))

In a connected component dispatch the socketConnect action on componentWillMount and you're good to go!

In part two we'll cover using Redux Sagas' eventChannel factory...