Syncing React State Across Tabs: Using Broadcast Channel API
Tags: javascript, react, node, webdev
Published: 2024-02-18 15:53:53

## Introduction 

One of the challenges that I find most complicated to solve is synchronizing the state of a React application between various tabs and windows. If we also consider service workers, it becomes even more challenging.

In today's article we are going to create a hook that will be very similar to what we do with `useState` but in reality behind the scenes we will take advantage of the [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API).

![react sync state between windows](https://res.cloudinary.com/dj5iihhqv/image/upload/v1707864309/uxsxbqpixvd0xofkvgep.gif)

## Getting Started

We start by running the following command:

```sh
npm create vite@latest
```

The template that was used was React with JavaScript.

Then we can open the `App.jsx` file and import the following primitives:

```jsx
import { useEffect, useState, useRef } from "react";

// ...
```

The next step is to create an abstraction layer with the creation of drivers, that is, I don't want the hook to be limited only by the use of [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Because in the future we may need to use cookies, IndexedDB, among others.

Which might look similar to this:

```jsx
import { useEffect, useState, useRef } from "react";

class LocalStorageManager {
  _storage;
  constructor() {
    this._storage = window.localStorage;
  }
  get(key) {
    const value = this._storage.getItem(key);
    return typeof value === "string" ? JSON.parse(value) : null;
  }
  set(key, value) {
    this._storage.setItem(key, JSON.stringify(value));
  }
}

// ...
```

Next we can move on to the hook definition, to do this we will create a function with three arguments:
- `key` - is the reference to the data that will be saved
- `initialData` - initial data of the hook when it is initially rendered in the component
- `driver` - manages side-effects and interactions with external resources such as API's

```jsx
// ...

function usePersistor(key, initialData, driver) {
  // ...
}
```

In the first step we will take advantage of the `useState` hook, initializing it with the initial state received in the arguments. Then we will create a reference to the `BroadcastChannel`, in which we will provide the contextual key that was also passed in the arguments.

```jsx
// ...

function usePersistor(key, initialData, driver) {
  const [storedData, _setStoredData] = useState(() => initialData);
  const _channel = useRef(new BroadcastChannel(key)).current;

  // ...
}
```

Now moving on to the definition of some functions, we start by creating the internal function `_readValue` which will obtain the data from the driver and if nothing is saved we return the initial data.

Another function that we will also need to create is `setValue` which will receive data as its only arguments and this will be saved in the local state (`storedData`), will be persisted from the driver and we will send a message from of `BroadcastChannel` to update the state between other tabs and windows.

```jsx
// ...

function usePersistor(key, initialData, driver) {
  const [storedData, _setStoredData] = useState(() => initialData);
  const _channel = useRef(new BroadcastChannel(key)).current;

  const _readValue = () => {
    const value = driver.get(key);
    return value ?? initialData;
  };

  const setValue = (data) => {
    driver.set(key, data);
    _setStoredData(data);
    _channel.postMessage(data);
  };

  // ...
}
```

One point we have to take into account is that the state is currently governed by the initial state provided in the `initialData` argument and we also have the possibility of obtaining the data from the driver through the internal function `_readValue`.

But we need to update the local state, if we have anything saved, to ensure that we have the updated state as soon as the component is mounted. To do this, we will take advantage of the `useEffect` hook as follows:

```jsx
// ...

function usePersistor(key, initialData, driver) {
  // ...

  useEffect(() => {
    const value = _readValue();
    _setStoredData(value);
  }, []);

  // ...
}
```

The only thing left to do is address the last point, which is updating the local state taking into account the messages that are sent through the `BroadcastChannel`. To do this we will also use a `useEffect` which will also only be executed as soon as the component is mounted to register an event listener that will listen to the `message` event and will update the state with `_setStoredData`.

Last but not least, we return a tuple with two elements: `storedData` and `setValue`.

```jsx
// ...

function usePersistor(key, initialData, driver) {
  // ...

  useEffect(() => {
    const value = _readValue();
    _setStoredData(value);
  }, []);

  useEffect(() => {
    function _listener({ data }) {
      _setStoredData(data);
    }

    _channel.addEventListener("message", _listener);
    return () => {
      _channel.removeEventListener("message", _listener);
    };
  }, []);

  return [storedData, setValue];
}
```

Here are some examples of how to use this hook. We need to instantiate the `LocalStorageManager` class while maintaining its reference. And then we can create two states within a component, one to synchronize the state of a counter and the other of an input.

```jsx
export default function App() {
  const driver = useRef(new LocalStorageManager()).current;
  const [count, setCount] = usePersistor("counter", 0, driver);
  const [name, setName] = usePersistor("name", "", driver);

  return (
    <section>
      <button onClick={() => setCount(count + 1)}>count is {count}</button>
      <br />
      <br />
      <input
        type="text"
        value={name}
        onChange={(evt) => setName(evt.target.value)}
      />
    </section>
  );
}
```

## Conclusion
I hope you found this article helpful, whether you're using the information in an existing project or just giving it a try for fun.

Please let me know if you notice any mistakes in the article by leaving a comment. And, if you'd like to see the source code for this article, you can find it on the github repository linked below.

[Github Repo](https://github.com/FranciscoMendes10866/react-broadcast-channel)