Write your own WebSocket middleware for React/Redux in 4 Steps
Tags: redux, react, javascript
Published: 2019-07-27 00:23:03

If you want to use websockets with redux and don't like dependencies, it's not too hard to write your own middleware as long as you understand some basic principles and how the pieces fit together. In this post I'll explain how to write your own websocket middleware and go through the entire websocket "flow" on the frontend. The code for this project can be found [here](https://github.com/aduranil/selfies)

### Step 1: Define an action that will be used to establish a websocket connection

I have defined a `const` that returns an object, or "action" of type `WS_CONNECT.`

`export const wsConnect = host => ({ type: 'WS_CONNECT', host });`

Some people choose to create an `actions.js` where they keep all of their actions. **I prefer to keep all of my redux actions, reducers, and functions in the same file, grouped by category.** Currently my project has 3 modules called websocket, game, and account. 

My websocket module looks like this, and it has my `WS_CONNECT` action:

```javascript
// modules/websocket.js 

export const wsConnect = host => ({ type: 'WS_CONNECT', host });
export const wsConnecting = host => ({ type: 'WS_CONNECTING', host });
export const wsConnected = host => ({ type: 'WS_CONNECTED', host });
export const wsDisconnect = host => ({ type: 'WS_DISCONNECT', host });
export const wsDisconnected = host => ({ type: 'WS_DISCONNECTED', host });

```

*Normally I would have a reducer here with something like `case WS_CONNECT`:, but I don't really need it for websockets because I don't need to save the data in my redux store. I'll show a case in the bonus section with an example of where it is helpful to have. 


### Step 2: Dispatch your action to open a new websocket connection

My project is similar to a chat application where people join rooms. Once they join the room, I want to establish a websocket connection to the room. **This is one approach, with another approach being wrapping your entire project in a websocket connection**, which I have an example of at the BONUS section of this post.

In the below example I establish a new websocket connection on `componentDidMount` when the user enters the room. I am using token authentication which is OK but **I suggest using session authentication** with websockets because you can't pass a token in a header. I'm dispatching the `wsConnect` function I defined above, but it's not going to do anything because I haven't written my middleware yet. 


```javascript 
// pages/Game.js
import React from 'react';
import { connect } from 'react-redux';
import { wsConnect, wsDisconnect } from '../modules/websocket';
import { startRound, leaveGame, makeMove } from '../modules/game';
import WithAuth from '../hocs/AuthenticationWrapper';

class Game extends React.Component {
  componentDidMount() {
    const { id } = this.props;
    if (id) {
      this.connectAndJoin();
    }
  }

  connectAndJoin = () => {
    const { id, dispatch } = this.props;
    const host = `ws://127.0.0.1:8000/ws/game/${id}?token=${localStorage.getItem('token')}`;
    dispatch(wsConnect(host));
  };


  render() {
    // abridged for brevity
    return `${<span> LOADING </span>}`;
  }

}

const s2p = (state, ownProps) => ({
  id: ownProps.match && ownProps.match.params.id,
});
export default WithAuth(connect(s2p)(Game));

```


### Step 3: Write the websocket middleware

Ok, so if you've done something similar to the above then you've written and dispatched an action, just like you would with normal redux. The only difference is you don't need to dispatch the action in the reducer (or at least i don't need to for this example). **However, nothing is happening yet.** You need to write the websocket middleware first. It's important to understand that **every action you dispatch will apply to every piece of middleware you have**. 

Here is my middleware file, while I'll break down in detail:

```javascript
//middleware/middleware.js 

import * as actions from '../modules/websocket';
import { updateGame, } from '../modules/game';

const socketMiddleware = () => {
  let socket = null;

  const onOpen = store => (event) => {
    console.log('websocket open', event.target.url);
    store.dispatch(actions.wsConnected(event.target.url));
  };

  const onClose = store => () => {
    store.dispatch(actions.wsDisconnected());
  };

  const onMessage = store => (event) => {
    const payload = JSON.parse(event.data);
    console.log('receiving server message');

    switch (payload.type) {
      case 'update_game_players':
        store.dispatch(updateGame(payload.game, payload.current_player));
        break;
      default:
        break;
    }
  };

  // the middleware part of this function
  return store => next => action => {
    switch (action.type) {
      case 'WS_CONNECT':
        if (socket !== null) {
          socket.close();
        }

        // connect to the remote host
        socket = new WebSocket(action.host);

        // websocket handlers
        socket.onmessage = onMessage(store);
        socket.onclose = onClose(store);
        socket.onopen = onOpen(store);

        break;
      case 'WS_DISCONNECT':
        if (socket !== null) {
          socket.close();
        }
        socket = null;
        console.log('websocket closed');
        break;
      case 'NEW_MESSAGE':
        console.log('sending a message', action.msg);
        socket.send(JSON.stringify({ command: 'NEW_MESSAGE', message: action.msg }));
        break;
      default:
        console.log('the next action:', action);
        return next(action);
    }
  };
};

export default socketMiddleware();
```

**Dispatch WS_CONNECT and make a new WebSocket().** Looking at the above, when I am dispatching the `WS_CONNECT` action, you can see that I have an `action.type` also called `WS_CONNECT` that establishes the websocket connection. The [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object comes installed with javascript. I establish a new connection with the host url that I passed in my action. 

**Javascript WebSocket API.** The javascript websocket API comes with three useful properties: `onmessage`, `onclose`, and `onopen.` In the above, I've created handlers to deal with all three of these, called `onMessage`, `onClose`, and `onOpen`, respectively. The most important one is `onmessage` which is an event handler for when a message is received from the server. The websocket API also has `close` and `send` functions which I use in my middleware. 

**Working with the server.** I won't go into the server side on this post, but the server sends the frontend plain objects with data, just how the frontend sends the server plain objects with data. in `onMessage`, which receives the server actions, I have defined an action on the server side called `update_game_players` which gives me information from the server, and then I dispatch a function called `updateGame` with an action of type `SET_GAME` to save that information to my redux store. 

```javascript 
// modules/game.js 
export const updateGame = (json, player) => ({ type: 'SET_GAME', data: json, player });


const gameInitialState = { time: null };

export const gameReducer = (state = { ...gameInitialState }, action) => {
  switch (action.type) {
    case 'SET_GAME':
      return { ...state, game: action.data, current_player: action.player };
    default:
      return state;
  }
```
You may be wondering what `default: return next(action)` does. As mentioned before, all actions are dispatch to all pieces of middleware. That means **if I have an action type that isn't relevant to my socket middleware, but is relevant to my normal redux middleware, I still need a way to handle it in the socket middleware**. The default part of the function just passes the action along. The below example can help illustrate:

![](https://thepracticaldev.s3.amazonaws.com/i/ektai3m4gnb8f5e9tucp.png)

When I type something in the chat, the frontend is sending an action called `NEW_MESSAGE` to the server with the data. The websocket server receives it and then sends a payload back to the frontend with a type of `update_game_players`, which essentially includes every relevant thing to the current game, including any new messages. When the frontend receives the action from the server, it dispatches an action called `updateGame` which has a type of `SET_GAME`. **That action dispatches, but the socket middleware doesn't have any handler for `SET_GAME` so it goes to the default case, while simultaneously going to the `SET_GAME` case in my default redux middleware.** 

### Step 4: Create the store with your new middleware 

This part is relatively straightforward. As shown in the below example, you are able to **create an array with all of your middleware** (I'm using my middleware I just created and the redux default) and then create the store using the `compose` and `createStore` functions that redux provides 

```javascript
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import reduxThunk from 'redux-thunk';
import rootReducer from './modules/reducers';
import wsMiddleware from './middleware/middleware';
import App from './App';

const middleware = [reduxThunk, wsMiddleware];
const store = createStore(
  rootReducer,
  compose(
    applyMiddleware(...middleware),
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
  ),
);
const Root = ({ store }) => (
  <Router>
    <Provider store={store}>
      <Route path="/" component={App} />
    </Provider>
  </Router>
);

ReactDOM.render(<Root store={store} />, document.getElementById('root'));
```

### BONUS: Wrap your entire project in a websocket connection

Here is an example of how to wrap you entire project in a websocket connection. This is another pattern that can also be used.

```javascript 
// index.js abridged example showing just the root

const store = // where you create your store 
const Root = ({ store }) => (
  <Router>
    <Provider store={store}>
      <WebSocketConnection
        host={`ws://127.0.0.1:8000/ws/game?token=${localStorage.getItem('token')}`}
      >
        <Route path="/" component={App} />
      </WebSocketConnection>
    </Provider>
  </Router>
);

ReactDOM.render(<Root store={store} />, document.getElementById('root'));
```

Here is my `WebSocketConnection` wrapper, which is very simple. It establishes the connection to the websocket
```javascript
// hocs/WebsocketConnection.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { wsConnect } from '../modules/websocket';

class WebSocketConnection extends Component {
  componentDidMount() {
    const { dispatch, host } = this.props;
      dispatch(wsConnect(host))
    }
  }

  render() {
    return <div>{this.props.children}</div>;
  }
}


export default connect()(WebSocketConnection);
```

My reducer is slightly different in this case. In step 2 above, I was doing all the server actions around joining a game at the same time as I established the websocket connection. In this example, I'm opening a general websocket connection first and joining the game as a separate action. This means that **I need to make sure my websocket connection has been established before trying to do anything else**, which is why I now want to see whether I'm connected or not.

```javascript
// modules/websocket.js 

const websocketInitialState = { connected: false };

export const websocketReducer = (state = { ...websocketInitialState }, action) => {
  switch (action.type) {
    case 'WS_CONNECTED':
      return { ...state, connected: true };
    default:
      return state;
  }
};
```

I can now use the `connected` prop to determine whether to dispatch actions. In the Game.js file I now do this 

```javascript
// pages/Game.js 

  componentDidMount() {
    const { id, connected } = this.props;
    if (connected) {
      this.connectAndJoin();
    }
  }

  connectAndJoin = async () => {
    const { id, dispatch } = this.props;
    await dispatch(joinGame(id));
  };
```