Using websockets with redux-saga

In this blog post, I share my approach in order to use websockets with redux-saga by using channels.

I assume that you already know all basics stuff about Sagas. If you don’t, read the introduction on the official documentation of redux-saga.

Redux-saga is mainly described as a way of dealing with side-effects (asynchronous calls and so on) performed by an application. Incoming actions from the store invoke a function that will dispatch later an other action back to the store. With websockets, we won’t rely on incoming actions but on external event sources. Let’s see how to achieve this.

Introducing channels

Here is a typical typical watch-and-fork pattern, in order to pull and handle actions :

function* watchRequests() {
while (true) {
const { payload } = yield take('REQUEST')
yield fork(handleRequest, payload)
}
}

We can implement the same flow with actionChannel :

import { take, actionChannel, call, ... } from 'redux-saga/effects'
function* watchRequests() {
const requestChan = yield actionChannel('REQUEST')
while (true) {
const { payload } = yield take(requestChan)
yield call(handleRequest, payload)
}
}

With the second pattern, we can take advantage of queueing all non-processed actions (inside requestChan).

With websockets, we also rely on a channel which is based on custom events rather than actions. To achieve this, we can make use of eventChannel.

eventChannel is a function that takes a subscriber to initialize the external event source (it means for us to connect to the websocket server).

Here is a rough example of how we can dispatch an action from a websocket event :

function websocketInitChannel() {
  return eventChannel( emitter => {
    // init the connection here
const ws = new WebSocket()

ws.onmessage = e => {
// dispatch an action with emitter here
return emitter( { type: 'ACTION_TYPE', payload } )
}
    // unsubscribe function
return () => {
// do whatever to interrupt the socket communication here
}
  })
}
export default function* websocketSagas() {
const channel = yield call(websocketInitChannel)
while (true) {
const action = yield take(channel)
yield put(action)
}
}

Websocket code should be isolated with a dedicated middleware at the redux store level :

const websocketMiddleware = createSagaMiddleware()
const middlewares = [ /*other_middlewares*/ , websocketMiddleware ]
const enhancers = [
applyMiddleware(...middlewares),
// ...
]
// ...
websocketMiddleware.run(websocketSagas)

An example from the real world

function initWebsocket() {
  return eventChannel(emitter => {
    ws = new WebSocket(wsUrl + '/client')
    ws.onopen = () => {
console.log('opening...')
ws.send('hello server')
}
    ws.onerror = (error) => {
console.log('WebSocket error ' + error)
console.dir(error)
}
    ws.onmessage = (e) => {
let msg = null
try {
msg = JSON.parse(e.data)
} catch(e) {
console.error(`Error parsing : ${e.data}`)
}
if (msg) {
const { payload: book } = msg
        const channel = msg.channel
switch (channel) {
case 'ADD_BOOK':
return emitter({ type: ADD_BOOK, book })
case 'REMOVE_BOOK':
return emitter({ type: REMOVE_BOOK, book })
default:
// nothing to do
}
}
}
    // unsubscribe function
return () => {
console.log('Socket off')
}
})
}
export default function* wsSagas() {
const channel = yield call(initWebsocket)
while (true) {
const action = yield take(channel)
yield put(action)
}
}

Keep in touch

If you have another method to integrate websockets in a Redux-based application, do not hesitate to share it as a comment.