48
How to syncing React state across multiple tabs with Redux
In the previous post of this series, we learn how to persist state across multiple tabs with simple usage of useState
hook and Window: storage event
features.
Now, let's go deeper and we'll see how to achieve the same behaviour, but with Redux state management.
In the case of applications developed in ReactJS that work with state control using Redux, or even useState and useContext hooks in simpler scenarios, by default, the context is kept separately for each active tab in the user's browser.
Unsynchronized State
import React from "react";
import ReactDOM from "react-dom";
import { Provider, connect } from "react-redux";
import { createStore } from "redux";
const Form = ({ name, handleChange }) => {
return (
<>
<input value={name} onChange={handleChange} />
</>
);
};
const reducer = (state, action) => {
switch (action.type) {
case "CHANGE":
return { ...state, name: action.payload };
default:
return state;
}
};
const store = createStore(reducer, { name: "" });
const mapStateToProps = (state) => {
return {
name: state.name,
};
};
const mapDispatchToProps = (dispatch) => {
return {
handleChange: (e) => dispatch({ type: "CHANGE", payload: e.target.value }),
};
};
const App = connect(mapStateToProps, mapDispatchToProps)(Form);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
For easy understanding, I choose to work with this minimum Redux implementation. I assume you already know React with Redux, if that's not your case, see the docs for more information.
Let’s add some extra packages to the project to achieve our goal:
npm i redux-state-sync redux-persist
redux-state-sync: will be used to sync redux state across tabs in realtime when state data is changed;
redux-persist: will be used to keep the redux state saved in the browser storage and allows reload the state again when the app is reloaded;
In this step, let's make some changes in our initial example to allow the app detect changes in the redux state, independently in which browser tab those changes happen, and keep state synced across all tabs where our app is opened.
The author of redux-state-sync
package defines it as:
A lightweight middleware to sync your redux state across browser tabs. It will listen to the Broadcast Channel and dispatch exactly the same actions dispatched in other tabs to keep the redux state in sync.
Although the author uses the Broadcast Channel API that is not supported on this date by all browsers, he was concerned to provide a fallback to make sure that the communication between tabs always works.
Synchronized State (without persist data on reload)
import React from "react";
import ReactDOM from "react-dom";
import { Provider, connect } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import {
createStateSyncMiddleware,
initMessageListener,
} from "redux-state-sync";
const Form = ({ name, handleChange }) => {
return (
<>
<input value={name} onChange={handleChange} />
</>
);
};
const reducer = (state, action) => {
switch (action.type) {
case "CHANGE":
return { ...state, name: action.payload };
default:
return state;
}
};
const store = createStore(
reducer,
{ name: "" },
applyMiddleware(createStateSyncMiddleware())
);
initMessageListener(store);
const mapStateToProps = (state) => {
return {
name: state.name,
};
};
const mapDispatchToProps = (dispatch) => {
return {
handleChange: (e) => dispatch({ type: "CHANGE", payload: e.target.value }),
};
};
const App = connect(mapStateToProps, mapDispatchToProps)(Form);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
Let's understand what has changed in this step...
import {
createStateSyncMiddleware,
initMessageListener,
} from "redux-state-sync";
First, we imported createStateSyncMiddleware
and initMessageListener
from redux-state-sync
package.
const store = createStore(
reducer,
{ name: "" },
applyMiddleware(createStateSyncMiddleware())
);
initMessageListener(store);
And then, we applied the State Sync middleware applyMiddleware(createStateSyncMiddleware())
when created redux store and started the message listener initMessageListener(store);
.
Now, redux state is synced across all tabs instantly! 🤗
Simple, isn't it? But as you can see, when the app is reloaded, redux state is lost. If you want to persist redux state even after browser reloading, stay here a little longer and let's go to the next step.
We'll use redux-persist
to persist and rehydrate our redux store.
Synchronized State (persisting data on reload)
import React from "react";
import ReactDOM from "react-dom";
import { Provider, connect } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import {
createStateSyncMiddleware,
initMessageListener,
} from "redux-state-sync";
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import { PersistGate } from "redux-persist/integration/react";
const Form = ({ name, handleChange }) => {
return (
<>
<input value={name} onChange={handleChange} />
</>
);
};
const reducer = (state, action) => {
switch (action.type) {
case "CHANGE":
return { ...state, name: action.payload };
default:
return state;
}
};
const persistConfig = {
key: "root",
storage,
};
const persistedReducer = persistReducer(persistConfig, reducer);
const store = createStore(
persistedReducer,
{ name: "" },
applyMiddleware(
createStateSyncMiddleware({
blacklist: ["persist/PERSIST", "persist/REHYDRATE"],
})
)
);
initMessageListener(store);
const mapStateToProps = (state) => {
return {
name: state.name,
};
};
const mapDispatchToProps = (dispatch) => {
return {
handleChange: (e) => dispatch({ type: "CHANGE", payload: e.target.value }),
};
};
const App = connect(mapStateToProps, mapDispatchToProps)(Form);
const persistor = persistStore(store);
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById("root")
);
Let's dive in it!
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import { PersistGate } from "redux-persist/integration/react";
-
persistStore
andpersistReducer
: basic usage involves adding persistReducer and persistStore to our setup; -
storage
: in case of web app, defaults to localStorage; -
PersistGate
: In React usage, we'll wrap our root component with PersistGate. As stated in the docs: This delays the rendering of your app's UI until your persisted state has been retrieved and saved to redux.
const persistConfig = {
key: "root",
storage,
};
const persistedReducer = persistReducer(persistConfig, reducer);
const store = createStore(
persistedReducer,
{ name: "" },
applyMiddleware(
createStateSyncMiddleware({
blacklist: ["persist/PERSIST", "persist/REHYDRATE"],
})
)
);
In createStore
, we replaced the old reducer
param by new customized reducer from package util persistedReducer
. We also need to blacklist some of the actions that is triggered by redux-persist, to State Sync middleware excludes them from syncronization.
const persistor = persistStore(store);
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById("root")
);
Lastly, we wrapped the root component with PersistGate
and pass persistor instance from persistStore
as props to component.
And everything works now...
In this series, we worked with pure client-side features to keep data synced across multiple tabs. Keeping React app data synced many times will also involve server-side features as realtime databases, websockets, etc.
Mixing all available tools to achieve our goals always will be the mindset to follow.
48