loading...

Write your own WebSocket middleware for React/Redux in 4 Steps

aduranil profile image Lina Rudashevski ・7 min read

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

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:

// 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.

// 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:

//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 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.

// 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:

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

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.

// 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

// 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.

// 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

// pages/Game.js 

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

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

Discussion

pic
Editor guide
 

Love it, thanks a lot for the excellent implementation! Quick question though, what's the point of this action which isn't called anywhere and isn't handled in the middleware nor reducer:
export const wsConnecting = host => ({ type: 'WS_CONNECTING', host });

 

Thanks for pointing this out, I do indeed have this defined but did not use it in my code. The original intention was to console.log when the websocket was in a connecting state but not yet connected, simply for logging and debugging purposes. I ended up not using it

 

hi, Thanks for this wonderful library. when I trigger WS_CONNECT automatically 'onClose' websocket handler calling. therefor before connecting with server it will automatically triggering in onClose WS_DISCONNECTED

 

Hi Lina, loved your post.

Quick doubt - the native JS websocket connection closes after 60 seconds of inactivity. I read almost everywhere to implement a ping-pong based fixed interval communication between clients and server to keep it alive. Do you agree or suggest something else around this problem?

 

Even sophisticated wrapper like socket.io have same problem if I choose the websocket mode of transportation with them instead of polling.

 
 

Great article, explains how to write middleware in a very straight forward manner.

 

Hi, could you please tell me which version of react and react-redux you have used for this example?
Thank you.