WebSockets in Redux: Using Sagas
Writing an event channel listener with Redux Saga.
By Ron Gierlach
In part one we wrote our own Redux middleware to manage WebSockets. In this post I'll be covering a second approach using Redux Saga.
For those of you unfamiliar with Redux Saga this could get a little confusing, however, some cursory examination of the documentation should get you up to speed.
At their basis, Sagas boil down to a pattern for handling side-effects using ES6 Generator functions. They aim to replace where otherwise you may have used a middleware like Redux Thunk in combination with async / await
or just some good ol' fashioned callbacks.
The main strategy in the case of an external event source like a WebSocket is to use the eventChannel
factory function -- it will take a subscriber function that establishes the event source (our WebSocket) and then emits the relevant events to our expectant saga. Read more on this here.
Presuming the same action types and action creators from the first post, let's write our Channel factory below:
import { eventChannel } from 'redux-saga'
import { bindActionCreators } from 'redux'
const connectSocket = ({
socketURL, // the url our socket connects to
subscribeData, // the handshake data our socket will send once connected (optional)
eventHandlers // the actions we want our socket to dispatch
}) => eventChannel(
emitter => {
// instantiate the web socket
const ws = new window.WebSocket(socketURl)
// bind eventHandlers to emitter
const boundEventHandlers = bindActionCreators(eventHandlers, emitter)
// emit 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
return ws.close
}
)
You've probably already noticed that the body of our Channel is nearly identical to the body of our middleware from the first post!
If you're still with me, let's take our Channel to task and use it in a Saga:
import { effects, takeEvery } from 'redux-saga'
const { call, put, take } = effects
// saga for our WebSocket
const socketSaga = function * (action) {
const socketChannel = yield call(connectSocket, action.payload)
while (true) {
const eventAction = yield take(socketChannel)
yield put(eventAction)
}
}
// rootSaga for our store, listens for 'SOCKET_CONNECT' dispatch
const rootSaga = function * (action) {
yield takeEvery(types.SOCKET_CONNECT, socketSaga)
}
Look at that while loop, what the heck is going on here?! It's actually not so crazy, I promise.
The yield
flag causes execution inside the generator function to pause (think of it as a generator function's version of a return
statement) before resuming iteration. This pattern allows us to respond to a constant stream of WebSocket-initiated action dispatches as they fire.
With our sagas at the ready, we just need to change our previously written socketConnect
action to return a payload with all our relevant WebSocket instantiation arguments.
const actionCreators = {
/* ...other action creators... */
socketConnect: opts => ({
type: types.SOCKET_CONNECT,
payload: {
socketURL: opts.socketURL,
subscribeData: opts.subscribeData,
eventHandlers: opts.eventHandlers
}
})
}
Note that these arguments could have been declared in the Channel factory itself or really at any stage prior to socket instantiation.
The only step left will be for you to add Redux Saga middleware to your store, instructions to which may be found here.
I hope you've found at least one of these approaches helpful and can start using WebSockets with your Redux application today!