DEV Community

loading...

How to syncing React state across multiple tabs with Redux

Cássio Lacerda
A self-taught fullstack Javascript developer who also speaks the language of Design and Marketing
Originally published at dev.to Updated on ・5 min read

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")
);
Enter fullscreen mode Exit fullscreen mode

Unsynchronized State

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.

1) Get the power!

Let’s add some extra packages to the project to achieve our goal:

npm i redux-state-sync redux-persist
Enter fullscreen mode Exit fullscreen mode

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;

2) Sync redux state across tabs

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")
);
Enter fullscreen mode Exit fullscreen mode

Let's understand what has changed in this step...

import {
  createStateSyncMiddleware,
  initMessageListener,
} from "redux-state-sync";
Enter fullscreen mode Exit fullscreen mode

First, we imported createStateSyncMiddleware and initMessageListener from redux-state-sync package.

const store = createStore(
  reducer,
  { name: "" },
  applyMiddleware(createStateSyncMiddleware())
);

initMessageListener(store);
Enter fullscreen mode Exit fullscreen mode

And then, we applied the State Sync middleware applyMiddleware(createStateSyncMiddleware()) when created redux store and started the message listener initMessageListener(store);.

Synchronized State (without persist data on reload)

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.

Let's go!

3) Persist redux state after browser reloading

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")
);

Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode
  • persistStore and persistReducer: 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"],
    })
  )
);
Enter fullscreen mode Exit fullscreen mode

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")
);

Enter fullscreen mode Exit fullscreen mode

Lastly, we wrapped the root component with PersistGate and pass persistor instance from persistStore as props to component.

And everything works now...

Synchronized State (with persist data on reload)

Conclusion

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.

Discussion (0)