Tutorial: Using Redux and Redux-Saga to handle WebSocket messages.
Tags: react, typescript, tutorial, redux
Published: 2020-02-14 14:41:46

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](https://github.com/mat-sz/filedrop-web) and it's been working well enough to make me consider writing a tutorial for it.

**Disclaimer:** I will be using [TypeSocket](https://github.com/mat-sz/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:

```ts
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):

```ts
export interface MessageModel {
    type: string,
};
```

This allows me to create an instance of TypeSocket:

```ts
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:

```ts
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...

```ts
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:

```ts
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:

```ts
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`:

```ts
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:

```ts
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.