DEV Community

Cover image for Syncing React State Across Tabs: Using Broadcast Channel API
Francisco Mendes
Francisco Mendes

Posted on

Syncing React State Across Tabs: Using Broadcast Channel API

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.

react sync state between windows

Getting Started

We start by running the following command:

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

The template that was used was React with JavaScript.

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

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

// ...
Enter fullscreen mode Exit fullscreen mode

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. Because in the future we may need to use cookies, IndexedDB, among others.

Which might look similar to this:

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

// ...
Enter fullscreen mode Exit fullscreen mode

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

function usePersistor(key, initialData, driver) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

// ...

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

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

// ...

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

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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:

// ...

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

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

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

// ...

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];
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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

Top comments (0)