Syncing Redux Stores across Browser Tabs
2020-05-06
This post was originally published on May 28, 2016 on Medium. I since migrated off from their platform and I'm republishing all of my old posts here.
I'm currently working on an web application which runs in two Chrome tabs/windows. The tabs have a leader-follower-relationship: So basically the leader tab is allowed to trigger state changes, where on the other hand the follower tab can only receive the changes and react to them e.g. displaying the changed data.
Redux is used to manage the app state and state changes. As a view layer react is utilized, though this is less important for the problem at hand: I need/want to keep my redux stores in sync with each other. An action dispatched on the leader store should update the follower store as well.
The obvious answer to this is to use something like websockets to send actions over the wire to a running websocket server, which will then forward the action to the follower application(s). The socket approach works well and is my first choice of a sync strategy; mainly due to the fact, that in my case there is already running a ‘websockety’ server to handle some other stuff (input from different peripherals attached to different machines).
In terms of code the setup looks something like this:
Sync Middleware
This middleware will take every action and send it off via websockets. This is very basic, of course you could apply some filters here or batch multiple actions etc.
Store setup
Your standard store setup, just drop in the sync middleware from above if this is the leader tab.
Store listener
A message event handler, which will dispatch actions coming in over the websocket. In a leader-follower setup actions will only flow in one direction. So, we got a pretty easy setup for now. We could of course complicate things and do this bidirectional having every tab sending AND receiving actions. With the code snippets above this will result in an infinite loop and your browser will crash :-) You can work around this by labeling the source of an action: If the tab receives an action over websockets which originated from itself, just don´t dispatch it on its store.
“But I don´t want to setup a websocket server”
So you or someone else want to demo your app on a different machine, which doesn´t run a local websocket server. Well there is an easy way out:
LocalStorage to the Rescue
All browser tabs/windows share the same localStorage object, which comes in pretty handy if you have an application running on multiple tabs. It gets even better if you learn about the ‘storage’ event the localStorage emits, when its data changes.
The devil lies in the detail, so this is what you can do:
Storage Middleware
The new middleware, which will just write the latest action to the localStorage. An important detail: The ‘storage’ event will only fire if the data you put in really did change. So if you have multiple actions with the same type and no payload (e.g. the well-known INCREMENT_COUNTER) and you just write them to the local storage the event won´t fire because the String stays the same. As you can see the easy workaround for this is just to always add a timestamp to the data before writing it to the storage: Every action will have a different timestamp, which leads to the ‘storage’ event firing every time.
Storage Listener
Similar to the websocket listener, we attach a listener to the window which will execute on ‘storage’ events. It will pull out the action from the event´s payload and dispatch it on the store.
That's it! You can easily implement both strategies and let them fallback on each other. Here is a small sample app using local storage to sync up: redux-tab-sync demo. Just open the app twice, so that you can see both windows and play around.