Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for How to sync React state across tabs with workers
JorensM
JorensM

Posted on • Edited on

     

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

letports=[];// All of our portsletlatest_state={}// The currently synced states.
Enter fullscreen modeExit fullscreen mode

So first we haveports 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 portsconstpostMessageAll=(msg,excluded_port=null)=>{ports.forEach(port=>{// Don't post message to the excluded port, if one has been specifiedif(port==excluded_port){return;}port.postMessage(msg);});}
Enter fullscreen modeExit fullscreen mode

What this does is simply post a provided mesage to all ports in theports array. You can also optionally provide anexcluded_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 anonconnect handler. Here we will define message handlers and some initialization logic

onconnect=(e)=>{constport=e.ports[0];ports.push(port);}
Enter fullscreen modeExit fullscreen mode

In the code above, we have created anonconnect handler and also made sure that the newly connected port gets been added to ourports array.

In theonconnect handler, let's add handlers for receiving messages

port.onmessage=(e)=>{// Sent by a tab to update state in other tabsif(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 modeExit 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 modeExit fullscreen mode

This handler will get called when a tab sends aset_state message, a.k.a when state is updated in one of the tabs. When this happens, we update ourlatest_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 ourget_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 modeExit fullscreen mode

What we do here is simply send aset_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 ouruseTabState hook.

useTabState.ts

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

Let's start writing our function:

exportdefaultfunctionuseTabState<T>(initial_state:T,id:string):[T,(new_state:T)=>void]{const[localState,setLocalState]=useState<T>(null);}
Enter fullscreen modeExit fullscreen mode

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

Then we create a state calledlocalState and set its initial value tonull

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 ofT toany. 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 thelocalState that updates the state across all tabs

constsetTabState=(new_state:T)=>{setLocalState(new_state);worker.port.postMessage({type:'set_state',id:id,state:new_state});}
Enter fullscreen modeExit fullscreen mode

What this function does is set its localstate as well as sends aset_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 auseEffect 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 modeExit 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 aset_state message has been received. Also we send aget_state message upon mount to get the up-to-date state. If the up-to-date state is null, then we use theinitial_state value.

Finally, let's return ourlocalState as well as thesetTabState function:

return[localState,setTabState];
Enter fullscreen modeExit fullscreen mode

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

index.tsx

importuseTabStatefrom'./useTabState';exportdefaultfunctionTabStateExample(){const[counter,setCounter]=useTabState<number>(0,'counter');return(<div><buttononClick={()=>setCounter(counter+1)}>{counter}</button></div>);}
Enter fullscreen modeExit 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 packagereact-tab-state

If you have any questions or feedback, feel free to leave it in the comments or email me atjorensmerenjanu@gmail.com

Good luck in your dev journey and your projects!

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Hi! My name is Jorens! I'm a developer/writer/musician/artist.
  • Location
    Riga, Latvia
  • Education
    Jelgava's technical school
  • Work
    Freelancer @ Upwork
  • Joined

More fromJorensM

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp