How to syncing React state across multiple tabs with Redux
Tags: react, javascript, redux
Published: 2021-07-16 14:02:23

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**

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

![Unsynchronized State](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lze2at1rxbsdflgprtoy.gif)

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](https://react-redux.js.org/introduction/getting-started) 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
```
**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](https://developer.mozilla.org/en-US/docs/Web/API/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)**

```jsx
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...

```jsx
import {
  createStateSyncMiddleware,
  initMessageListener,
} from "redux-state-sync";
```
First, we imported `createStateSyncMiddleware` and `initMessageListener` from `redux-state-sync` package.

```jsx
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);`.

![Synchronized State (without persist data on reload)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/etennhn4a1bv0r9gv5rf.gif)

Now, redux state is synced across all tabs instantly! &#x1F917;

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!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ki7lylbnrkduzj81amkd.gif)

## 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)**

```jsx
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!

```jsx
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import { PersistGate } from "redux-persist/integration/react";
```
- `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.*

```jsx
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.

``` jsx
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...

![Synchronized State (with persist data on reload)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nz1clnzuxmhyiahmr4vw.gif)

## 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.