Shared Redux state

I’ve been using Redux in React applications for over a year now. The common approaches are to fetch data from a server using RESTful-apis or GraphQL and rendering the initial state on page loads.

But what if multiple clients need to share state? For example an online game or a messaging app. There are many ways to accomplish this. In this article I’m going to make a very simple implementation of a shared state using Redux middleware and web sockets for communication.

Redux

Redux is very simple. A model, or state, in redux is a plain object. A change in the state requires an action. The action describes the intention and to perform the change a reducer takes the current state and action and produces the next state. The reducer must always produce the same state given the same current state and action, it must be a pure function. A common pattern is to compose multiple reducers that act on separate parts of the state, i.e. properties of the state object.

Let’s start with a state model like this:

{
  shared: {
    messages: [‘Hello’, ‘World’ ],
  },
  user: {
    name: ‘Me’,
  },
}

The messages part will be shared between clients but not the user-object. I want to be able to build the application first without sharing anything, just a local state. We’ll start by creating an action and a reducer called ‘shared’.

const say = message => ({
  type: ‘SAY’,
  message,
});

const shared = (state = { message: [] }, action) => {
  switch (action.type) {
    case ‘SAY’:
      return { messages: [ ...state.messages, action.message ] };
    default:
      return state;
  }
}

The action ‘SAY’ simply adds a string to the end of the array of messages. It’s now easy to build a React-application that connects a component to the state, displays the messages and offers an input for the action. However, I will not further explain any of that here.

The shared-reducer is then combined with other reducers by:

const reducer = combineReducers({
  shared,
  user,
};

Now, how do we make this a shared model?

A middleware is a way to enhance the redux store. An example of middleware is Thunk that make action asynchronous. A middleware is a good place for logic that should be executed after the action is dispatched, but before the reducer is called.

The idea is to use a middleware to dispatch the actions to the server through web sockets. Actions are plain objects and are well suited to transfer as JSON. For the middleware to know which action that are remote and which are local, we add a property to the action. Let’s create a function that does this for us.

const remoteAction = action => {
  action.remote = true;
  return action;
};

This way we can easily change the implementation of the say-action like this:

const say = message => remoteAction({
  type: ‘SAY’,
  message,
});

The middleware

Let’s move on to middleware. It should send the actions to the server if the property remote is true and otherwise dispatch it to the local store. For this, we need to create a web socket where we can send actions and when the server sends messages, use these as action to the local store.

const remoteDispatch = (url, localDispatch) => {
  const ws = new WebSocket(url);
  ws.onmessage = message => {
    localDispatch(JSON.parse(message.data));
  };
  return action => ws.send(JSON.stringify(action));
};

A middleware is a function with the store as argument that returns a function with the next action handler as argument, which in turn returns a function with action as argument. The first argument is not the actual store; it’s just an object with the functions getState and dispatch. The dispatch is what we need to connect the web socket to our store when we receive messages from the server.

const remoteMiddleware = url => store => {
  const dispatch = remoteDispatch(url, store.dispatch);
  return next => action => {
    if (action.remote) {
      dispatch(action);
    }
    else {
      next(action);
    }
  };
};

We can now create the remoteMiddleware with an URL to the web socket. But wait, how will the server do updates? What action should it send to change the state on the client?
To make it simple in this example, we’ll create an action and reducer that completely replaces the state. I’m going to make a function that adapts an existing reducer by wrapping it.

const remoteReducer = (reducer, name) => {
  return (state, action) => {
    if (action.type === ‘REMOTE_UPDATE’) {
      return action.state[name];
    }
    else {
      // Call the wrapper reducer
      return reducer(state, action);
    }
  }
}

The server can now send a REMOTE_UPDATE action with the complete state. The argument name must match the property of the state for which the reducer operates on. We previously combined reducers using the redux function combineReducers. Let’s make our own, a function that accepts an object of local reducers and one for remote reducers.

const remoteCombineReducers = (localReducers, remoteReducers) => {
  // Loop through the remote reducers and adapt them
  Object.keys(remoteReducers).forEach(key => {
    remoteReducers[key] = remoteReducer(remoteReducers[key], key);
  });
  return combineReducers({ ...localReducers, ...remoteReducers });
}

By looping the keys of remoteReducers we automatically know the property name for the reducer and adapts every remote reducer. We then use the standard combineReducers to combine the local and remote reducers.

Server side

So, the client side reducers and actions are prepared. We started out by implementing the reducers to work in a client-only state. We don’t want to create new reducers for the server state, we want to reuse them. To use the same definition of the store on the server we can make some small changes to our remote-functions. First of all, since we added a property called remote on actions sent to the server, the server must treat actions as a local actions. Therefore the middleware and combination of reducers needs to be modified. By creating the store with the middleware separately for client and server, we only need to look att the reducers. Assume we have a variable to indicate if the code is running in the server or on the client, isServer.

const remoteCombineReducers = (localReducers, remoteReducers) => {
  if (isServer) {
    return combineReducers(remoteReducers);
  }
  Object.keys(remoteReducers).forEach(key => {
    remoteReducers[key] = remoteReducer(remoteReducers[key], key);
  });
  return combineReducers({ ...localReducers, ...remoteReducers });
}

The remoteReducers on the client-side are local on the server so just combine those reducers without adapting them and ignore the local reducers for the client. The only thing left for a working solution is to create the server web socket connected to the store. When a new client connects, it should start by sending the current state to bring the client up to speed.

const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const store = createStore(reducer);

wss.on(‘connection’, ws => {
  ws.on(‘message’, message => store.dispatch(JSON.parse(message)));
  ws.send(JSON.stringify(remoteUpdate(store.getState())));
});

store.subscribe(() => {
  const action = remoteUpdate(store.getState());
  wss.clients.forEach(ws => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(action));
    }
  });
});

That’s it. The server and client can send actions back and forth.

The example code is available online in https://github.com/tobhult/remote-redux-app/. Just download, install and run it to try it out.

Note: this example is not ready for production. Aspects like fault tolerance, error handling and security has not been addressed.