diff --git a/frontend/src/contexts/auth.context.tsx b/frontend/src/contexts/auth.context.tsx index a68651a4..41e4c81f 100644 --- a/frontend/src/contexts/auth.context.tsx +++ b/frontend/src/contexts/auth.context.tsx @@ -21,11 +21,7 @@ import { Progress } from "@/app-components/displays/Progress"; import { useLogout } from "@/hooks/entities/auth-hooks"; import { useApiClient } from "@/hooks/useApiClient"; import { CURRENT_USER_KEY, PUBLIC_PATHS } from "@/hooks/useAuth"; -import { - EBCEvent, - ETabMode, - useBroadcastChannel, -} from "@/hooks/useBroadcastChannel"; +import { useBroadcastChannel } from "@/hooks/useBroadcastChannel"; import { useTranslate } from "@/hooks/useTranslate"; import { RouterType } from "@/services/types"; import { IUser } from "@/types/user.types"; @@ -64,7 +60,6 @@ export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { i18n.changeLanguage(lang); }; const { mutate: logoutSession } = useLogout(); - const { mode, value } = useBroadcastChannel(); const logout = async () => { updateLanguage(publicRuntimeConfig.lang.default); logoutSession(); @@ -113,11 +108,9 @@ export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { setIsReady(true); }, []); - useEffect(() => { - if (value === EBCEvent.LOGOUT_END_SESSION && mode === ETabMode.SECONDARY) { - router.reload(); - } - }, [value, mode, router]); + useBroadcastChannel("session", () => { + router.reload(); + }); if (!isReady || isLoading) return ; diff --git a/frontend/src/hooks/entities/auth-hooks.ts b/frontend/src/hooks/entities/auth-hooks.ts index fb78fed4..1dc65bf0 100755 --- a/frontend/src/hooks/entities/auth-hooks.ts +++ b/frontend/src/hooks/entities/auth-hooks.ts @@ -22,7 +22,7 @@ import { useSocket } from "@/websocket/socket-hooks"; import { useFind } from "../crud/useFind"; import { useApiClient } from "../useApiClient"; import { CURRENT_USER_KEY, useAuth, useLogoutRedirection } from "../useAuth"; -import { EBCEvent, useBroadcastChannel } from "../useBroadcastChannel"; +import { useBroadcastChannel } from "../useBroadcastChannel"; import { useToast } from "../useToast"; import { useTranslate } from "../useTranslate"; @@ -59,7 +59,7 @@ export const useLogout = ( const { logoutRedirection } = useLogoutRedirection(); const { toast } = useToast(); const { t } = useTranslate(); - const { send } = useBroadcastChannel(); + const postDisconnectionSignal = useBroadcastChannel("session"); return useMutation({ ...options, @@ -70,7 +70,7 @@ export const useLogout = ( }, onSuccess: async () => { queryClient.removeQueries([CURRENT_USER_KEY]); - send(EBCEvent.LOGOUT_END_SESSION); + postDisconnectionSignal("logout"); await logoutRedirection(); toast.success(t("message.logout_success")); }, diff --git a/frontend/src/hooks/useBroadcastChannel.ts b/frontend/src/hooks/useBroadcastChannel.ts index beba882d..7caaa69d 100644 --- a/frontend/src/hooks/useBroadcastChannel.ts +++ b/frontend/src/hooks/useBroadcastChannel.ts @@ -6,52 +6,136 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { useEffect, useRef, useState } from "react"; +import * as React from "react"; -export enum ETabMode { - PRIMARY = "primary", - SECONDARY = "secondary", -} +export type BroadcastChannelData = + | string + | number + | boolean + | Record + | undefined + | null; -export enum EBCEvent { - LOGOUT_END_SESSION = "logout-end-session", -} +/** + * React hook to create and manage a Broadcast Channel across multiple browser windows. + * + * @param channelName Static name of channel used across the browser windows. + * @param handleMessage Callback to handle the event generated when `message` is received. + * @param handleMessageError [optional] Callback to handle the event generated when `error` is received. + * @returns A function to send/post message on the channel. + * @example + * ```tsx + * import {useBroadcastChannel} from 'react-broadcast-channel'; + * + * function App () { + * const postUserIdMessage = useBroadcastChannel('userId', (e) => alert(e.data)); + * return (); + * } + * ``` + * --- + * Works in browser that support Broadcast Channel API natively. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API#browser_compatibility). + * To support other browsers, install and use [broadcastchannel-polyfill](https://www.npmjs.com/package/broadcastchannel-polyfill). + */ +export function useBroadcastChannel( + channelName: string, + handleMessage?: (event: MessageEvent) => void, + handleMessageError?: (event: MessageEvent) => void, +): (data: T) => void { + const channelRef = React.useRef(null); -export const useBroadcastChannel = ( - channelName: string = "main-broadcast-channel", - initialValue?: EBCEvent, -) => { - const channelRef = useRef(null); - const [value, setValue] = useState(initialValue); - const [mode, setMode] = useState(ETabMode.PRIMARY); - - useEffect(() => { - channelRef.current = new BroadcastChannel(channelName); + React.useEffect(() => { + if (typeof window !== "undefined" && "BroadcastChannel" in window) { + channelRef.current = new BroadcastChannel(channelName + "-channel"); + } }, [channelName]); - useEffect(() => { - if (channelRef.current) { - channelRef.current.addEventListener("message", (event) => { - if (mode === ETabMode.PRIMARY) { - setValue(event.data); - setMode(ETabMode.SECONDARY); - } - }); - channelRef.current.postMessage(initialValue); + useChannelEventListener(channelRef.current, "message", handleMessage); + useChannelEventListener( + channelRef.current, + "messageerror", + handleMessageError, + ); + + return React.useCallback( + (data: T) => channelRef.current?.postMessage(data), + [channelRef.current], + ); +} + +/** + * React hook to manage state across browser windows. Has the similar signature as `React.useState`. + * + * @param channelName Static name of channel used across the browser windows. + * @param initialState Initial state. + * @returns Tuple of state and setter for the state. + * @example + * ```tsx + * import {useBroadcastState} from 'react-broadcast-channel'; + * + * function App () { + * const [count, setCount] = useBroadcastState('count', 0); + * return ( + *
+ * + * {count} + * + *
+ * ); + * } + * ``` + * --- + * Works in browser that support Broadcast Channel API natively. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API#browser_compatibility). + * To support other browsers, install and use [broadcastchannel-polyfill](https://www.npmjs.com/package/broadcastchannel-polyfill). + */ +export function useBroadcastState( + channelName: string, + initialState: T, +): [T, React.Dispatch>, boolean] { + const [isPending, startTransition] = React.useTransition(); + const [state, setState] = React.useState(initialState); + const broadcast = useBroadcastChannel(channelName, (ev) => + setState(ev.data), + ); + const updateState: React.Dispatch> = + React.useCallback( + (input) => { + setState((prev) => { + const newState = typeof input === "function" ? input(prev) : input; + + startTransition(() => broadcast(newState)); + + return newState; + }); + }, + [broadcast], + ); + + return [state, updateState, isPending]; +} + +// Helpers + +/** Hook to subscribe/unsubscribe from channel events. */ +function useChannelEventListener( + channel: BroadcastChannel | null, + event: K, + handler?: (e: BroadcastChannelEventMap[K]) => void, +) { + const callbackRef = React.useRef(handler); + + if (callbackRef.current !== handler) { + callbackRef.current = handler; + } + + React.useEffect(() => { + const callback = callbackRef.current; + + if (!channel || !callback) { + return; } - return () => { - if (channelRef.current?.onmessage) { - channelRef.current.onmessage = null; - } - channelRef.current?.close(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [channelName, initialValue]); + channel.addEventListener(event, callback); - const send = (data: EBCEvent) => { - channelRef.current?.postMessage(data); - }; - - return { mode, value, send }; -}; + return () => channel.removeEventListener(event, callback); + }, [channel, event]); +}