How to Sync a Redux Store Across Browser Tabs – CloudSavvy IT

Illustration showing the Redux logo

Redux is a convenient way to manage state in complex web apps. Your Redux store isn’t synced across tabs though, which can lead to some awkward scenarios. If the user logs out in one tab, it would be ideal for that action to be reflected in their other open tabs.

You can add cross-tab store synchronisation using the react-state-sync library. This provides a middleware which automatically syncs the store across tabs. You don’t need to manually intervene in the sync process after the initial setup.

Overview

The Redux State Sync library offers a Redux middleware which you need to add to your store. When the store receives an action, the middleware will dispatch the same action in any other tabs open to your site.

Each tab sets up its own message listener to receive incoming action notifications. When the listener is notified, it will dispatch the action into the tab’s store.

Messages are exchanged between tabs using the Broadcast Channel API. This is a browser feature which allows tabs from the same origin to communicate with each other. Tabs “subscribe” to “channels.” They’re notified by the browser when another tab posts a message to a subscribed channel.

Broadcast Channel is only available in newer browsers. Redux State Sync uses an abstraction which can fallback to alternative technologies. When Broadcast Channel is unsupported, IndexedDB or LocalStorage will be used instead.

The use of Broadcast Channel places some limitations on what you can send between tabs. Data you dispatch must be supported by the browser’s structured clone algorithm. This includes scalar values, arrays, plain JavaScript objects and blobs. Complex values will not be transferred exactly.

Synchronising Your Store

To start using Redux State Sync, add it to your project via npm:

npm install redux-state-sync

Next, create a basic store. We’ll modify this code in a moment to add Redux State Sync.

import {createStore} from "redux";
 
const state = {authenticated: false};
 
const reducer = (state, action) => ({...state, ...action});
 
const store = createStore(reducer, state);

Now we have a simple store. To sync it across tabs, add the Redux State Sync middleware and setup a message listener.

import {createStore, applyMiddleware} from "redux";
import {createStateSyncMiddleware, initMessageListener} from "redux-state-sync";
 
const reduxStateSyncConfig = {};
 
const reducer = (state, action) => ({...state, ...action});
 
const store = createStore(
    reducer,
    state,
    applyMiddleware(createStateSyncMiddleware(reduxStateSyncConfig))
);
 
initMessageListener(store);

The store is now ready to use across tabs. Open your site in two tabs. Dispatch an action in one of the tabs. You should see the action appear in both stores, effecting the corresponding state change in each. The Redux DevTools extension can be used to monitor incoming actions and the state changes they cause.

The state sync middleware is created using the createStateSyncMiddleware() utility function. This accepts a config object used to customise Redux State Sync’s operation. We’ll look more closely at this in the next section.

After the store is created, it’s passed to initMessageListener(). This function ensures the cross-tab listening is configured. Without this call, tabs may not receive new actions if no action was dispatched on first load.

Using initMessageListener() won’t give your tab access to the existing store held in another tab. When the user opens a new tab, it’ll default to having its own fresh store. If you want new tabs to get their state from an open tab, use the initStateWithPrevTab() function instead.

const store = createStore(reducer, state, applyMiddleware(createStateSyncMiddleware({})));
initStateWithPrevTab(store);

The store’s state will be replaced with the existing state if there’s another open tab available.

Customised Synchronisation

Redux State Sync supports several configuration options to let you customise the synchronisation. Here’s some of the most useful settings. Each one is set as a property in the config object passed to createStateSyncMiddleware().

Excluding Actions

Sometimes you’ll have actions which you don’t want to synchronise. An example could be an action which causes a modal dialog to appear. Chances are you don’t want this dialog to show up in all the user’s open tabs!

You can exclude specific named actions using the blacklist option. Pass an array of action names as the value.

const config = {
    blacklist: ["DEMO_ACTION"]
};
 
// ...
 
// This won't be synced to any other tabs
Store.dispatch({type: "DEMO_ACTION"});

You can also use a whitelist instead of a blacklist. Set the whitelist config option to allow only predefined actions to be synchronised.

Precisely Filtering Actions

If neither blacklist or whitelist give you enough control, set the predicate option. This accepts a function which is used to filter synchronisable actions.

const config = {
    predicate: action => (action.type !== "DEMO_ACTION")
};

The function will be invoked each time a new action is received. It’ll receive the action as a parameter. The function should return true or false to indicate whether the action should be synchronised to other tabs. The example above will synchronise every action except DEMO_ACTION.

Broadcast Channel Settings

You can change the name of the Broadcast Channel by setting the channel property. It defaults to redux_state_sync. You shouldn’t usually need to change this unless you want to have two separate synchronisation routines.

You can pass options to the Broadcast Channel abstraction library by setting broadcastChannelOption. This should be a configuration object accepted by the pubkey/broadcast-channel library.

You can use this to force a particular storage technology to be used. In this example, synchronisation will always occur via IndexedDB, even if the browser has native support for Broadcast Channels.

const config = {
    broadcastChannelOption: {type: "idb"}
};

Integrating With Redux-Persist

You’ll often want to use Redux State Sync in conjunction with Redux Persist. Redux Persist is a popular library which automatically persists your Redux store in the browser.

When using Redux Persist, you don’t need to use Redux Persist’s initStateWithPrevTab() function. Use initMessageListener() instead, as the initial state will always be the persisted state provided by Redux Persist.

Blacklist Redux Persist actions within your Redux State Sync configuration. These don’t need to be synchronised across tabs. You should only sync changes that actually affect the store, rather than actions related to its lifecycle.

const config = {
    blacklist: ["persist/PERSIST", "persist/REHYDRATE"]
};

Summary

Redux State Sync lets you synchronise user actions across tabs. The applications are virtually boundless and are likely to improve the user experience. As users take actions on your site, they’ll be immediately reflected in their other open tabs.

The “classic” use case is synchronising login and logout outcomes. There are other benefits too though, such as making incoming notifications available to all tabs, or synchronising client-side preferences like the user’s selected UI theme.

The minified redux-state-sync library weighs in at 19KB. With setup consisting of just a few extra lines of code, you should consider adding Redux State Sync to your next project. It lets you link tabs together into a cohesive whole, instead of having them exist as independent entities.