useSynchronizedState
The useSynchronizedState
hook allows you to synchronize async or sync states across different tabs or windows in a web application using the Broadcast Channel API. This is useful for sharing state between multiple instances of your application.
Add the utility
npx @ivnatsr/ezreact add use-synchronized-state
import { useState, useEffect, useRef, useCallback } from 'react'
export function useSynchronizedState({ initialState, key, track }) { const [state, setState] = useState(initialState) const emitterChannelRef = useRef(null) const receiverChannelRef = useRef(null) const lastTrackedState = useRef(undefined) const isFirstRender = useRef(true)
if (lastTrackedState.current === undefined && isFirstRender.current) { lastTrackedState.current = typeof initialState === 'function' ? initialState() : initialState }
const broadcast = useCallback((message) => { if ( emitterChannelRef.current !== null && JSON.stringify(message) !== JSON.stringify(lastTrackedState.current) ) { lastTrackedState.current = message emitterChannelRef.current.postMessage(message) } }, [])
useEffect(() => { if (emitterChannelRef.current === null && receiverChannelRef.current === null) { emitterChannelRef.current = new BroadcastChannel(key) receiverChannelRef.current = new BroadcastChannel(key) }
const onMessage = (event) => { setState(event.data) }
const onMessageError = () => { console.error(`Error receiving message on channel with key: ${key}`) }
receiverChannelRef.current?.addEventListener('message', onMessage) receiverChannelRef.current?.addEventListener('messageerror', onMessageError)
isFirstRender.current = false
return () => { receiverChannelRef.current?.removeEventListener('message', onMessage) receiverChannelRef.current?.removeEventListener('messageerror', onMessageError) } }, [key])
useEffect(() => { return () => { if (receiverChannelRef.current !== null && emitterChannelRef.current !== null) { emitterChannelRef.current.close() receiverChannelRef.current.close() emitterChannelRef.current = null receiverChannelRef.current = null } } }, [])
if (track !== undefined) broadcast(track)
return { state, broadcast }}
import { useState, useEffect, useRef, useCallback } from 'react'
type UseSynchronizedStateOptions<StateType> = { key: string initialState?: StateType | (() => StateType) track?: StateType}
export function useSynchronizedState<StateType>({ initialState, key, track}: UseSynchronizedStateOptions<StateType>) { const [state, setState] = useState<StateType | undefined>(initialState) const emitterChannelRef = useRef<BroadcastChannel | null>(null) const receiverChannelRef = useRef<BroadcastChannel | null>(null) const lastTrackedState = useRef<StateType | undefined>(undefined) const isFirstRender = useRef(true)
if (lastTrackedState.current === undefined && isFirstRender.current) { lastTrackedState.current = typeof initialState === 'function' ? (initialState as () => StateType)() : initialState }
const broadcast = useCallback((message: StateType) => { if ( emitterChannelRef.current !== null && JSON.stringify(message) !== JSON.stringify(lastTrackedState.current) ) { lastTrackedState.current = message emitterChannelRef.current.postMessage(message) } }, [])
useEffect(() => { if (emitterChannelRef.current === null && receiverChannelRef.current === null) { emitterChannelRef.current = new BroadcastChannel(key) receiverChannelRef.current = new BroadcastChannel(key) }
const onMessage = (event: MessageEvent<StateType>) => { setState(event.data) }
const onMessageError = () => { console.error(`Error receiving message on channel with key: ${key}`) }
receiverChannelRef.current?.addEventListener('message', onMessage) receiverChannelRef.current?.addEventListener('messageerror', onMessageError)
isFirstRender.current = false
return () => { receiverChannelRef.current?.removeEventListener('message', onMessage) receiverChannelRef.current?.removeEventListener('messageerror', onMessageError) } }, [key])
useEffect(() => { return () => { if (receiverChannelRef.current !== null && emitterChannelRef.current !== null) { emitterChannelRef.current.close() receiverChannelRef.current.close() emitterChannelRef.current = null receiverChannelRef.current = null } } }, [])
if (track !== undefined) broadcast(track)
return { state, broadcast }}
Parameters
key
: A string that serves as the key for the Broadcast Channel.initialState
(optional): The initial state value or a function that returns the initial state.track
(optional): A state value that, when changed, will trigger a broadcast to other tabs or windows. Useful for tracking asynchronous states. If provided, you don’t need to manually broadcast updates through the channel, since the hook will perform this action automatically.
Return
This hook returns an object containing:
state
: The current synchronized state.broadcast
: A function that can be called to manually broadcast a new state value.
Example
import { useRef } from 'react'import { useSynchronizedState } from './path/to/use-synchronized-state'
const SynchronizedStateExample = () => { const count = useRef(0) const { state, broadcast } = useSynchronizedState({ key: 'example-channel', initialState: count.current })
const increment = () => { broadcast(++count.current) }
return ( <div> <h1>Current Count: {state}</h1> <button onClick={increment}>Increment</button> </div> )}
export default SynchronizedStateExample