Since I've discovered redux-saga I've found that it's perfect for asynchronous actions that affect the global state - and handling WebSocket messages is one of those things. The first time I've used this is in filedrop-web and it's been working well enough to make me consider writing a tutorial for it.
Disclaimer: I will be using TypeSocket, which is a library I've made. It makes certain WebSocket-related tasks easier without being too heavy (no special polyfills for platforms that don't support WS).
You can get TypeSocket from npm:
yarn add typesocket
# or
npm install typesocket
The way my integration works is by creating a new Redux middleware that will contain the WebSocket handling code, will dispatch WebSocket messages and connection state updates and will react to incoming send message actions.
First, I have an ActionType enum, for all the available ActionTypes:
enum ActionType {
WS_CONNECTED = 'WS_CONNECTED',
WS_DISCONNECTED = 'WS_DISCONNECTED',
WS_MESSAGE = 'WS_MESSAGE',
WS_SEND_MESSAGE = 'WS_SEND_MESSAGE',
};
Then I also define an interface for the message model (TypeSocket will reject all invalid JSON messages by default, but doesn't check if the message matches your type):
export interface MessageModel {
type: string,
};
This allows me to create an instance of TypeSocket:
import { TypeSocket } from 'typesocket';
const socket = new TypeSocket<MessageModel>(url);
Which is what we'll be using within our middleware. url
refers to the WebSocket URL.
Writing a Redux middleware around TypeSocket is really simple, first we create an empty middleware:
import { MiddlewareAPI } from 'redux';
import { TypeSocket } from 'typesocket';
import { ActionType } from './types/ActionType'; // Your enum with action types.
import { MessageModel } from './types/Models'; // Your message model.
export const socketMiddleware = (url: string) => {
return (store: MiddlewareAPI<any, any>) => {
// Here we will create a new socket...
// ...and handle the socket events.
return (next: (action: any) => void) => (action: any) => {
// And here we'll handle WS_SEND_MESSAGE.
return next(action);
};
};
};
Now all that's left is adding our TypeSocket construction code into the middleware...
export const socketMiddleware = (url: string) => {
return (store: MiddlewareAPI<any, any>) => {
const socket = new TypeSocket<MessageModel>(url);
// We still need the events here.
return (next: (action: any) => void) => (action: any) => {
// And here we'll handle WS_SEND_MESSAGE.
return next(action);
};
};
};
...and adding the event handling and message sending:
export const socketMiddleware = (url: string) => {
return (store: MiddlewareAPI<any, any>) => {
const socket = new TypeSocket<MessageModel>(url);
// We dispatch the actions for further handling here:
socket.on('connected', () => store.dispatch({ type: ActionType.WS_CONNECTED }));
socket.on('disconnected', () => store.dispatch({ type: ActionType.WS_DISCONNECTED }));
socket.on('message', (message) => store.dispatch({ type: ActionType.WS_MESSAGE, value: message }));
socket.connect();
return (next: (action: any) => void) => (action: any) => {
// We're acting on an action with type of WS_SEND_MESSAGE.
// Don't forget to check if the socket is in readyState == 1.
// Other readyStates may result in an exception being thrown.
if (action.type && action.type === ActionType.WS_SEND_MESSAGE && socket.readyState === 1) {
socket.send(action.value);
}
return next(action);
};
};
};
Now that this is taken care of, we need to add the middlewarae to our store. Let's first save the middleware in src/socketMiddleware.ts
.
Then we can use it like this:
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { socketMiddleware } from './socketMiddleware';
import reducers, { StoreType } from './reducers';
import sagas from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducers,
applyMiddleware(socketMiddleware('ws://localhost:5000/'), sagaMiddleware),
);
sagaMiddleware.run(sagas, store.dispatch);
I'm assuming that there are reducers available from ./reducers
and sagas (for Redux Saga) in ./sagas
.
Now, let's start using Redux Saga to handle our messages. This is pretty simple and comes down to utilizing Redux-Saga's takeEvery
:
function* message(action: ActionModel) {
const msg: MessageModel = action.value as MessageModel;
// Now we can act on incoming messages
switch (msg.type) {
case MessageType.WELCOME:
yield put({ type: ActionType.WELCOME, value: 'Hello world!' });
break;
}
}
export default function* root(dispatch: (action: any) => void) {
yield takeEvery(ActionType.WS_MESSAGE, message);
}
Sending messages with our setup is also that easy, you will just have to dispatch the message like so:
dispatch({ type: Action.WS_SEND_MESSAGE, value: message });
I prefer using this method over using any other Redux WebSocket libraries because of the flexibility I get when it comes to handling actions inside of the middleware, there's a lot of things you can customize. TypeSocket can be replaced for a pure WebSocket as well, if necessary.
Top comments (1)
Have you tried
?
I mean - to meta-tagg messages so you can intercept only tagged messages at middleware level and send them over wss?
Also, how do you sync message signatures with the back-end? I have an rough idea to try generate TS message typings from protobufs alongside with swagger docs. But i didn't tried that yet.