Logo

dev-resources.site

for different kinds of informations.

How to sync React state across tabs with workers

Published at
12/18/2023
Categories
tab
sync
state
react
Author
jorensm
Categories
4 categories in total
tab
open
sync
open
state
open
react
open
Author
7 person written this
jorensm
open
How to sync React state across tabs with workers

Hello, in this article I will show you how you can sync state in React across multiple tabs. We will be using the SharedWorker API to achieve this. If you want to skip the tutorial and just use it, I have an npm package react-tab-state

The tutorial is written in TypeScript (aside from the worker), but you can just as well use JavaScript for this.

This is what the final result will look like:

Image description

So without further ado, let's get started!

Project structure

I will assume that you already know how to set up a React project. You can use CRA or Vite or even NextJS, just make sure that the environment supports shared workers(it probably does)!

For this project we will have 3 main files -

  • index.tsx, where our page code will reside
  • useTabState.ts, where the code for our tab- synced-state hook will reside
  • worker.js, where the code for our shared worker will reside

worker.js

First of all let's write the code of our worker.

First let's add some variables



let ports = []; // All of our ports
let latest_state = {} // The currently synced states.


Enter fullscreen mode Exit fullscreen mode

So first we have ports which stores all of open ports. There is a single port for each tab. A port is basically a connection between a tab and the shared worker, which allows you to send and receive messages. We need this array so we can send a single message to all ports at once

Next, latest_state stores the most recent synced state values in an object. Each key in the object will be a unique id for that particular state. This is so we can have multiple states. We will need this variable in order to sync the state of a newly opened tab

Now let's add a function to post a message to all ports



// Post message to all connected ports
const postMessageAll = (msg, excluded_port = null) => {
    ports.forEach( port => {
        // Don't post message to the excluded port, if one has been specified
        if(port == excluded_port){
              return;
        }
        port.postMessage(msg);
    });
}


Enter fullscreen mode Exit fullscreen mode

What this does is simply post a provided mesage to all ports in the ports array. You can also optionally provide an excluded_port, to which the message won't be sent. This is so that when a tab wants to update state in the rest of the tabs, you don't send a message back to the initiator tab.

Let's write an onconnect handler. Here we will define message handlers and some initialization logic



onconnect = (e) => {
    const port = e.ports[0];
    ports.push(port);
}


Enter fullscreen mode Exit fullscreen mode

In the code above, we have created an onconnect handler and also made sure that the newly connected port gets been added to our ports array.

In the onconnect handler, let's add handlers for receiving messages



port.onmessage = (e) => {
    // Sent by a tab to update state in other tabs
    if (e.data.type == 'set_state') {

    }
    // Sent by a tab to request the value of current state. Used when initializing the state.
    if (e.data.type == 'get_state') {

    }
}


Enter fullscreen mode Exit fullscreen mode

What the code above does is create a message handler for the connected port. So each time a tab sends a message to the worker, this handler will be invoked. Then in the handler we check the type of message and act accordingly.

Now let's add a handler for each message



if (e.data.type == 'set_state') {
    latest_state[e.data.id] = e.data.state;

    postMessageAll({
        type: 'set_state',
        id: e.data.id,
        state: latest_state[e.data.id]
    }, port);
 }


Enter fullscreen mode Exit fullscreen mode

This handler will get called when a tab sends a set_state message, a.k.a when state is updated in one of the tabs. When this happens, we update our latest_state with the state that the tab has sent us for the key with a matching ID, as well as send the new state to all the other tabs. We exclude the tab that initially sent the message because it already has the new state.

Next let's add our get_state handler. It will be called when a tab with our page first opens, to retrieve the up-to-date state value.



if (e.data.type == 'get') {
    port.postMessageAll({
        type: 'set_state',
        id: e.data.id,
        state: latest_state[e.data.id]
    });
}


Enter fullscreen mode Exit fullscreen mode

What we do here is simply send a set_state message to the tab that requested it, with our up-to-date state.

And that's it for the worker! Pretty simple, right? We're about halfway done.

Now let's write our useTabState hook.

useTabState.ts

Our useTabState will be used the same way as a regular useState - you call the hook and it will return an array with the state and a setState() function

Let's start writing our function:



export default function useTabState<T>(initial_state: T, id: string): [T, (new_state: T) => void]{
  const [localState, setLocalState] = useState<T>(null);
}


Enter fullscreen mode Exit fullscreen mode

So far we have made a hook that accepts 2 arguments - initial_state for the initial state and id for a unique ID to distinguish between multiple tab states.

Then we create a state called localState and set its initial value to null

You may have noticed that we're using generics here. If you're using TypeScript but don't know how to use generics or don't want to use them, you can remove the <T> and change any occurence of T to any. If you're using plain JavaScript then you can remove the <T> and omit the types completely.

Next up let's create a function below the localState that updates the state across all tabs



const setTabState = (new_state: T) => {
    setLocalState(new_state);
    worker.port.postMessage({
        type: 'set_state',
        id: id,
        state: new_state
    });
}


Enter fullscreen mode Exit fullscreen mode

What this function does is set its localstate as well as sends a set_state message to the worker, resulting in state being synced across all tabs. This is the function that our hook will return.

We're almost done! Now all we have to do is create a listener for when the shared worker sends a message to the tab.

Create a useEffect and add the following code to it:



useEffect(() => {
    worker.port.addEventListener('message', e => {
        if (!e.data?.type) {
            return;
        }

        switch (e.data.type){
            case 'set_state': {
                if (e.data.id == id) {
                    if (e.data.state) {
                        setLocalState(e.data.state);
                    } else {
                        setLocalState(initial_state);
                    }

                }
            }
        }

    })

    worker.port.start(); // This is important, otherwise the messages won't be received.

    worker.port.postMessage({
        type: 'get_state',
        id: id
    });
}, []);


Enter fullscreen mode Exit fullscreen mode

As you can see in the code above, we add a message event listener and update our local state with the up-to-date state when a set_state message has been received. Also we send a get_state message upon mount to get the up-to-date state. If the up-to-date state is null, then we use the initial_state value.

Finally, let's return our localState as well as the setTabState function:



return [localState, setTabState];


Enter fullscreen mode Exit fullscreen mode

And we're done! Now all that is left is to test our hook!

index.tsx



import useTabState from './useTabState';

export default function TabStateExample() {
    const [counter, setCounter] = useTabState<number>(0, 'counter');

    return (
        <div>
            <button
                onClick={() => setCounter(counter + 1)}
            >
                {counter}
            </button>
        </div>
    );
}


Enter fullscreen mode Exit fullscreen mode

In the code above, we simply create a button that shows a number, and increments this number each time it is clicked. Try testing this page with multiple tabs!

Conclusion

In this article we explored a simple way how one can add a tab-synced state behavior to their app using the SharedWorker API. I hope you learned something new and that you will find a use for this pattern. If you don't want to go through the hassle of learning and implementing this yourself (though it's quite simple), you can use my NPM package react-tab-state

If you have any questions or feedback, feel free to leave it in the comments or email me at [email protected]

Good luck in your dev journey and your projects!

state Article's
30 articles in total
Favicon
Svelte 5: Share state between components (for Dummies)
Favicon
Pampanga State Agricultural University
Favicon
Data Flow in LLM Applications: Building Reliable Context Management Systems
Favicon
Props and State in React
Favicon
Radar Market Innovations: Phased Array Solid-State Radar Development
Favicon
A single state for Loading/Success/Error in NgRx
Favicon
Advanced State Management - XState
Favicon
Top 7 Tips for Managing State in JavaScript Applications 🌟
Favicon
MithrilJS component with state management
Favicon
React State Management: When & Where add your states?
Favicon
STATE MANAGEMENT IN REACT
Favicon
State Management with Zustand
Favicon
A practical summary of React State variables & Props!
Favicon
State in React
Favicon
Weak memoization in Javascript
Favicon
Crafting a Global State Hook in React
Favicon
Reusing state management: HOC vs Hook
Favicon
State Vs Prop in React [Tabular Difference]
Favicon
Mastering XState Fundamentals: A React-powered Guide
Favicon
Does limiting state matter on the FrontEnd?
Favicon
Reducer vs. Finite State Machines: Understanding the Paradigm Shift
Favicon
A tool that can be used by anyone to manage React Query state externally
Favicon
Taming the State Beast: Redux vs. Recoil in React
Favicon
11 friends of state management in Angular
Favicon
React State Management
Favicon
How Can State Management Be Applied To A Real World Case-Scenario
Favicon
No more State Management with Signals
Favicon
How to keep state between page refreshes in React
Favicon
How to sync React state across tabs with workers
Favicon
State Management Patterns in React

Featured ones: