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));
};
Top comments (9)
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 onCloseWS_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.
Awesome breakdown!
thanks
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.