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 new file mode 100644 index 0000000000..24d15d65ad --- /dev/null +++ b/packages/hooks/src/useBroadcastChannel/__tests__/index.test.ts @@ -0,0 +1,51 @@ +import { act, renderHook } from '@testing-library/react'; +import useBroadcastChannel from '../index'; + +// 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; + 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..3777bf99db --- /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..6ed7a4567f --- /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 default 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` | - |