Handling Outgoing Messages and Actions

Learn how to forward actions in WebSockets.

We must consider how and when to pass actions from Redux to the server:

// A function to hold the WebSocket functionality
return action => {
  // TODO: Pass action to server

  next(action);
};

Before sending any actions, we need to make sure that the WebSocket is open and ready for transmissions. WebSockets have a readyState property that returns the current socket status:

// Checking if the socket is open
const SOCKET_STATES = {
  CONNECTING: 0,
  OPEN: 1,
  CLOSING: 2,
  CLOSED: 3
};

if (websocket.readyState === SOCKET_STATES.OPEN) {
  // Send
}

Even when the socket is open, not all actions have to be sent (for example, actions like TAB_SELECTED or REST_API_COMPLETE). It is best to leave the decision of what to send to our action creators. The standard way to provide special information about actions to middleware is to use the meta key inside an action. Thus, instead of using a regular action creator:

// A regular action creator
export const localAction = (data) => ({
  type: TEST,
  data
});

We can add special information to the metadata part of the action:

// An action creator to use in websocket
export const serverAction = (data) => ({
  type: TEST,
  data,
  meta: { websocket: true }
});

This way, our middleware can use the meta.websocket field to decide whether to pass the action on or not:

// Sending actions to the server
return action => {
  if (websocket.readyState === SOCKET_STATES.OPEN &&
      action.meta &&
      action.meta.websocket) {
    websocket.send(JSON.stringify(action));
  }

  next(action);
};

Note: This code might cause a surprising bug. Because we are sending the whole action to the server, it might broadcast the action to all other clients. And because we didn’t remove the action’s meta information, the other clients’ WebSocket middleware might rebroadcast the action repeatedly.

A Redux-aware server should consider stripping all meta information for any action it receives. In our implementation we will remove this on the client side, though the server should still do the check:

// Sending actions to server without metadata
return next => action => {
  if (websocket.readyState === SOCKET_STATES.OPEN &&
      action.meta &&
      action.meta.websocket) {

    // Remove action metadata before sending
    const cleanAction = Object.assign({}, action, { meta: undefined });
    websocket.send(JSON.stringify(cleanAction));
  }

  next(action);
};

Using this approach, sending actions to our server via a WebSocket becomes as simple as setting the meta.websocket field to true.

Here is the complete code:

import { wsConnected, wsDisconnected } from 'actions';
import { WS_ROOT } from 'const/global';

const SOCKET_STATES = {
  CONNECTING: 0,
  OPEN: 1,
  CLOSING: 2,
  CLOSED: 3
};

const wsMiddleware = ({ dispatch }) => next => {

 const websocket = new WebSocket(WS_ROOT);

 Object.assign(websocket, {
   onopen: () => dispatch(wsConnected()),
   onclose: () => dispatch(wsDisconnected()),
   onerror: (error) => console.log(`WS Error: ${ error.data }`),
   onmessage: (event) => dispatch(JSON.parse(event.data))
 });

 return action => {
   if (websocket.readyState === SOCKET_STATES.OPEN &&
       action.meta &&
       action.meta.websocket) {

     // Remove action metadata before sending
     const cleanAction = Object.assign({}, action, {
       meta: undefined
     });
     
     websocket.send(JSON.stringify(cleanAction));
   }

   next(action);
};

export default wsMiddleware;

Get hands-on with 1200+ tech skills courses.