From d60f7770880ef2790256e324c0253e9bbf7e9cdc Mon Sep 17 00:00:00 2001 From: Muhamed-Ragab Date: Sun, 6 Jul 2025 01:47:57 +0300 Subject: [PATCH 1/3] feat: add useBroadcastChannel hook This commit introduces the `useBroadcastChannel` hook, which enables communication between different browser tabs or windows using the BroadcastChannel API. The hook provides a simple interface for sending and receiving messages, with options for handling messages and errors. A demo and tests have also been added to showcase the functionality of the hook. --- .../__tests__/index.test.ts | 47 +++++++++++++++++ .../src/useBroadcastChannel/demo/demo1.tsx | 50 +++++++++++++++++++ .../src/useBroadcastChannel/index.en-US.md | 38 ++++++++++++++ .../hooks/src/useBroadcastChannel/index.ts | 26 ++++++++++ .../src/useBroadcastChannel/index.zh-CN.md | 38 ++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts create mode 100644 packages/hooks/src/useBroadcastChannel/demo/demo1.tsx create mode 100644 packages/hooks/src/useBroadcastChannel/index.en-US.md create mode 100644 packages/hooks/src/useBroadcastChannel/index.ts create mode 100644 packages/hooks/src/useBroadcastChannel/index.zh-CN.md diff --git a/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts b/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts new file mode 100644 index 0000000000..8f41e60387 --- /dev/null +++ b/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts @@ -0,0 +1,47 @@ +import { act, renderHook } from '@testing-library/react'; +import { useBroadcastChannel } from '../index'; + +describe('useBroadcastChannel', () => { + it('should send and receive messages', () => { + const channelName = 'test-channel'; + let received: string | null = null; + const { result: sender } = renderHook(() => + useBroadcastChannel(channelName) + ); + const { result: receiver } = renderHook(() => + useBroadcastChannel(channelName, { + onMessage: msg => { + received = msg; + }, + }) + ); + + act(() => { + sender.current.sendMessage('hello'); + }); + + expect(received).toBe('hello'); + }); + + it('should support multiple messages', () => { + const channelName = 'multi-message-channel'; + const messages: string[] = []; + const { result: sender } = renderHook(() => + useBroadcastChannel(channelName) + ); + renderHook(() => + useBroadcastChannel(channelName, { + onMessage: msg => { + messages.push(msg); + }, + }) + ); + + act(() => { + sender.current.sendMessage('foo'); + sender.current.sendMessage('bar'); + }); + + expect(messages).toEqual(['foo', 'bar']); + }); +}); diff --git a/packages/hooks/src/useBroadcastChannel/demo/demo1.tsx b/packages/hooks/src/useBroadcastChannel/demo/demo1.tsx new file mode 100644 index 0000000000..b1e2977836 --- /dev/null +++ b/packages/hooks/src/useBroadcastChannel/demo/demo1.tsx @@ -0,0 +1,50 @@ +import useLocalStorageState from '../../useLocalStorageState'; +import { useBroadcastChannel } from '../index'; + +interface IAuthMessage { + type: 'LOGOUT'; + timestamp: number; +} + +const App = () => { + const [state, setState] = useLocalStorageState('auth_token', { + defaultValue: '', + }); + const authChannel = useBroadcastChannel('auth_channel', { + onMessage: message => { + if (message.type === 'LOGOUT') { + console.log( + 'Received logout message at:', + new Date(message.timestamp).toLocaleTimeString() + ); + setState(''); + } + }, + onMessageError: event => { + console.error('Message error', event.data); + }, + }); + + const logoutWithBroadcast = () => { + authChannel.sendMessage({ + type: 'LOGOUT', + timestamp: Date.now(), + }); + setState(''); + }; + + return ( +
+ + +

Current Auth Token: {state}

+
+ ); +}; + +export default App; diff --git a/packages/hooks/src/useBroadcastChannel/index.en-US.md b/packages/hooks/src/useBroadcastChannel/index.en-US.md new file mode 100644 index 0000000000..d8294ccb5c --- /dev/null +++ b/packages/hooks/src/useBroadcastChannel/index.en-US.md @@ -0,0 +1,38 @@ +--- +nav: + path: /hooks +--- + +# useBroadcastChannel + +A hook for communicating between browser tabs or windows using the [BroadcastChannel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API). + +## Examples + +### Basic usage + + + +## API + +```typescript +const [state, { + postMessage, + close, +}] = useBroadcastChannel(channelName, options?); +``` + +### Result + +| Property | Description | Type | +| ----------- | ---------------------------------------- | ------------------ | +| state | Last message received from the channel | `T` | +| postMessage | Send a message to the channel | `(msg: T) => void` | +| close | Close the channel and clean up listeners | `() => void` | + +### Params + +| Property | Description | Type | Default | +| ----------- | ---------------------------------------- | -------- | ------- | +| channelName | Name of the broadcast channel | `string` | - | +| options | Optional configuration (e.g., onMessage) | `object` | - | diff --git a/packages/hooks/src/useBroadcastChannel/index.ts b/packages/hooks/src/useBroadcastChannel/index.ts new file mode 100644 index 0000000000..418a0b4c7b --- /dev/null +++ b/packages/hooks/src/useBroadcastChannel/index.ts @@ -0,0 +1,26 @@ +import { useEffect, useRef } from 'react'; + +interface BroadcastChannelOptions { + onMessage?: (message: T) => void; + onMessageError?: (event: MessageEvent) => void; +} + +export function useBroadcastChannel( + channelName: string, + options: BroadcastChannelOptions = {} +) { + const channel = new BroadcastChannel(channelName); + const channelRef = useRef(channel); + const currentChannel = channelRef.current; + + const sendMessage = (data: T) => currentChannel.postMessage(data); + + useEffect(() => { + currentChannel.onmessage = event => options.onMessage?.(event.data); + currentChannel.onmessageerror = event => options.onMessageError?.(event); + }, [channelName, currentChannel, options]); + + return { + sendMessage, + }; +} diff --git a/packages/hooks/src/useBroadcastChannel/index.zh-CN.md b/packages/hooks/src/useBroadcastChannel/index.zh-CN.md new file mode 100644 index 0000000000..d910da9d2e --- /dev/null +++ b/packages/hooks/src/useBroadcastChannel/index.zh-CN.md @@ -0,0 +1,38 @@ +--- +nav: + path: /hooks +--- + +# useBroadcastChannel + +用于在浏览器标签页或窗口之间通信的 Hook,基于 [BroadcastChannel API](https://developer.mozilla.org/zh-CN/docs/Web/API/Broadcast_Channel_API)。 + +## 代码演示 + +### 基础用法 + + + +## API + +```typescript +const [state, { + postMessage, + close, +}] = useBroadcastChannel(channelName, options?); +``` + +### Result + +| 参数 | 说明 | 类型 | +| ----------- | -------------------- | ------------------ | +| state | 最近一次收到的消息 | `T` | +| postMessage | 发送消息到频道 | `(msg: T) => void` | +| close | 关闭频道并清理监听器 | `() => void` | + +### Params + +| 参数 | 说明 | 类型 | 默认值 | +| ----------- | ----------------------------- | -------- | ------ | +| channelName | 频道名称 | `string` | - | +| options | 可选配置(如 onMessage 回调) | `object` | - | From 321b23481b90faed0e2d66c4812f3748b3ac6b3c Mon Sep 17 00:00:00 2001 From: Muhamed-Ragab Date: Sun, 6 Jul 2025 16:16:28 +0300 Subject: [PATCH 2/3] test: skip tests for useBroadcastChannel if BroadcastChannel is unavailable --- packages/hooks/src/index.ts | 134 +++++++++--------- .../__tests__/index.test.ts | 6 +- 2 files changed, 73 insertions(+), 67 deletions(-) diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 55c7232b0d..d94ee41232 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -2,6 +2,7 @@ import { createUpdateEffect } from './createUpdateEffect'; import useAntdTable from './useAntdTable'; import useAsyncEffect from './useAsyncEffect'; import useBoolean from './useBoolean'; +import useBroadcastChannel from './useBroadcastChannel'; import useClickAway from './useClickAway'; import useControllableValue from './useControllableValue'; import useCookieState from './useCookieState'; @@ -41,6 +42,7 @@ import useMap from './useMap'; import useMemoizedFn from './useMemoizedFn'; import useMount from './useMount'; import useMouse from './useMouse'; +import useMutationObserver from './useMutationObserver'; import useNetwork from './useNetwork'; import usePagination from './usePagination'; import usePrevious from './usePrevious'; @@ -59,6 +61,7 @@ import useSet from './useSet'; import useSetState from './useSetState'; import useSize from './useSize'; import useTextSelection from './useTextSelection'; +import useTheme from './useTheme'; import useThrottle from './useThrottle'; import useThrottleEffect from './useThrottleEffect'; import useThrottleFn from './useThrottleFn'; @@ -74,88 +77,87 @@ import useUpdateLayoutEffect from './useUpdateLayoutEffect'; import useVirtualList from './useVirtualList'; import useWebSocket from './useWebSocket'; import useWhyDidYouUpdate from './useWhyDidYouUpdate'; -import useMutationObserver from './useMutationObserver'; -import useTheme from './useTheme'; export { - useRequest, - useControllableValue, - useDynamicList, - useVirtualList, - useResponsive, - useEventEmitter, - useLocalStorageState, - useSessionStorageState, - useSize, + clearCache, configResponsive, - useUpdateEffect, - useUpdateLayoutEffect, + createUpdateEffect, + useAntdTable, + useAsyncEffect, useBoolean, - useToggle, - useDocumentVisibility, - useSelections, - useThrottle, - useThrottleFn, - useThrottleEffect, - useDebounce, - useDebounceFn, - useDebounceEffect, - usePrevious, - useMouse, - useScroll, + useBroadcastChannel, useClickAway, - useFullscreen, - useInViewport, - useKeyPress, - useEventListener, - useHover, - useUnmount, - useSet, - useMemoizedFn, - useMap, + useControllableValue, + useCookieState, + useCountDown, + useCounter, useCreation, + useDebounce, + useDebounceEffect, + useDebounceFn, + useDeepCompareEffect, + useDeepCompareLayoutEffect, + useDocumentVisibility, useDrag, useDrop, - useMount, - useCounter, - useUpdate, - useTextSelection, + useDynamicList, + useEventEmitter, + useEventListener, useEventTarget, + useExternal, + useFavicon, + useFocusWithin, + useFullscreen, + useFusionTable, + useGetState, useHistoryTravel, - useCookieState, - useSetState, + useHover, + useInfiniteScroll, useInterval, - useWhyDidYouUpdate, - useTitle, - useNetwork, - useTimeout, - useReactive, - useFavicon, - useCountDown, - useWebSocket, - useLockFn, - useUnmountedRef, - useExternal, - useSafeState, - useLatest, + useInViewport, useIsomorphicLayoutEffect, - useDeepCompareEffect, - useDeepCompareLayoutEffect, - useAsyncEffect, + useKeyPress, + useLatest, + useLocalStorageState, + useLockFn, useLongPress, - useRafState, - useTrackedEffect, + useMap, + useMemoizedFn, + useMount, + useMouse, + useMutationObserver, + useNetwork, usePagination, - useAntdTable, - useFusionTable, - useInfiniteScroll, - useGetState, - clearCache, - useFocusWithin, - createUpdateEffect, + usePrevious, useRafInterval, + useRafState, useRafTimeout, + useReactive, + useRequest, useResetState, - useMutationObserver, + useResponsive, + useSafeState, + useScroll, + useSelections, + useSessionStorageState, + useSet, + useSetState, + useSize, + useTextSelection, useTheme, + useThrottle, + useThrottleEffect, + useThrottleFn, + useTimeout, + useTitle, + useToggle, + useTrackedEffect, + useUnmount, + useUnmountedRef, + useUpdate, + useUpdateEffect, + useUpdateLayoutEffect, + useVirtualList, + useWebSocket, + useWhyDidYouUpdate, }; diff --git a/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts b/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts index 8f41e60387..e341633ac8 100644 --- a/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts +++ b/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts @@ -1,7 +1,11 @@ import { act, renderHook } from '@testing-library/react'; import { useBroadcastChannel } from '../index'; -describe('useBroadcastChannel', () => { +// Skip tests if BroadcastChannel is not available (i.e., not running in a browser) +const describeOrSkip = + typeof BroadcastChannel === 'undefined' ? describe.skip : describe; + +describeOrSkip('useBroadcastChannel', () => { it('should send and receive messages', () => { const channelName = 'test-channel'; let received: string | null = null; From a4fff26323713ace6d409e61daf15f3a278de151 Mon Sep 17 00:00:00 2001 From: Muhamed-Ragab Date: Sun, 13 Jul 2025 12:32:53 +0300 Subject: [PATCH 3/3] fix: update import statement for useBroadcastChannel to default export --- packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts | 2 +- packages/hooks/src/useBroadcastChannel/demo/demo1.tsx | 2 +- packages/hooks/src/useBroadcastChannel/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts b/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts index e341633ac8..24d15d65ad 100644 --- a/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts +++ b/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts @@ -1,5 +1,5 @@ import { act, renderHook } from '@testing-library/react'; -import { useBroadcastChannel } from '../index'; +import useBroadcastChannel from '../index'; // Skip tests if BroadcastChannel is not available (i.e., not running in a browser) const describeOrSkip = diff --git a/packages/hooks/src/useBroadcastChannel/demo/demo1.tsx b/packages/hooks/src/useBroadcastChannel/demo/demo1.tsx index b1e2977836..3777bf99db 100644 --- a/packages/hooks/src/useBroadcastChannel/demo/demo1.tsx +++ b/packages/hooks/src/useBroadcastChannel/demo/demo1.tsx @@ -1,5 +1,5 @@ import useLocalStorageState from '../../useLocalStorageState'; -import { useBroadcastChannel } from '../index'; +import useBroadcastChannel from '../index'; interface IAuthMessage { type: 'LOGOUT'; diff --git a/packages/hooks/src/useBroadcastChannel/index.ts b/packages/hooks/src/useBroadcastChannel/index.ts index 418a0b4c7b..6ed7a4567f 100644 --- a/packages/hooks/src/useBroadcastChannel/index.ts +++ b/packages/hooks/src/useBroadcastChannel/index.ts @@ -5,7 +5,7 @@ interface BroadcastChannelOptions { onMessageError?: (event: MessageEvent) => void; } -export function useBroadcastChannel( +export default function useBroadcastChannel( channelName: string, options: BroadcastChannelOptions = {} ) {