From ce5fba15e0da21010e67e91c207d966a8ad58899 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Wed, 22 Jan 2025 16:41:49 +0100 Subject: [PATCH] Add generic way to use channel state with the help of useSyncExternalStore hook --- package.json | 6 ++- .../hooks/useChannelMembershipState.ts | 43 ++++++---------- .../hooks/useSelectedChannelState.ts | 49 +++++++++++++++++++ yarn.lock | 27 +++++----- 4 files changed, 81 insertions(+), 44 deletions(-) create mode 100644 src/components/ChannelList/hooks/useSelectedChannelState.ts diff --git a/package.json b/package.json index a1c076194..0b07395f8 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,8 @@ "textarea-caret": "^3.1.0", "tslib": "^2.6.2", "unist-builder": "^3.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit": "^5.0.0", + "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@stream-io/transliterate": "^1.5.5", @@ -207,6 +208,7 @@ "@types/react-image-gallery": "^1.2.4", "@types/react-is": "^18.2.4", "@types/textarea-caret": "3.0.0", + "@types/use-sync-external-store": "^0.0.6", "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", @@ -257,7 +259,7 @@ "react-dom": "^18.1.0", "react-test-renderer": "^18.1.0", "semantic-release": "^19.0.5", - "stream-chat": "^8.50.0", + "stream-chat": "link:../stream-chat-js/", "ts-jest": "^29.1.4", "typescript": "^5.4.5" }, diff --git a/src/components/ChannelList/hooks/useChannelMembershipState.ts b/src/components/ChannelList/hooks/useChannelMembershipState.ts index faf48d1b2..fb80c0766 100644 --- a/src/components/ChannelList/hooks/useChannelMembershipState.ts +++ b/src/components/ChannelList/hooks/useChannelMembershipState.ts @@ -1,28 +1,15 @@ -import { useEffect, useState } from 'react'; -import type { Channel, ChannelState, ExtendableGenerics } from 'stream-chat'; - -import { useChatContext } from '../../../context'; - -export const useChannelMembershipState = ( - channel?: Channel, -) => { - const [membership, setMembership] = useState['membership']>( - channel?.state.membership || {}, - ); - - const { client } = useChatContext(); - - useEffect(() => { - if (!channel) return; - - const subscriptions = ['member.updated'].map((v) => - client.on(v, () => { - setMembership(channel.state.membership); - }), - ); - - return () => subscriptions.forEach((subscription) => subscription.unsubscribe()); - }, [client, channel]); - - return membership; -}; +import type { Channel, ChannelMemberResponse, EventTypes, ExtendableGenerics } from 'stream-chat'; +import { useSelectedChannelState } from './useSelectedChannelState'; + +const selector = (c: Channel) => c.state.membership; +const keys: EventTypes[] = ['member.updated']; + +export function useChannelMembershipState( + channel: Channel, +): ChannelMemberResponse; +export function useChannelMembershipState( + channel?: Channel | undefined, +): ChannelMemberResponse | undefined; +export function useChannelMembershipState(channel?: Channel) { + return useSelectedChannelState({ channel, selector, stateChangeEventKeys: keys }); +} diff --git a/src/components/ChannelList/hooks/useSelectedChannelState.ts b/src/components/ChannelList/hooks/useSelectedChannelState.ts new file mode 100644 index 000000000..bf1e67730 --- /dev/null +++ b/src/components/ChannelList/hooks/useSelectedChannelState.ts @@ -0,0 +1,49 @@ +import { useCallback } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; +import type { Channel, EventTypes, ExtendableGenerics } from 'stream-chat'; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; + +export function useSelectedChannelState(_: { + channel: Channel; + selector: (channel: Channel) => O; + stateChangeEventKeys?: EventTypes[]; +}): O; +export function useSelectedChannelState(_: { + selector: (channel: Channel) => O; + channel?: Channel | undefined; + stateChangeEventKeys?: EventTypes[]; +}): O | undefined; +export function useSelectedChannelState({ + channel, + stateChangeEventKeys = ['all'], + selector, +}: { + selector: (channel: Channel) => O; + channel?: Channel; + stateChangeEventKeys?: EventTypes[]; +}): O | undefined { + const subscribe = useCallback( + (onStoreChange: (value: O) => void) => { + if (!channel) return noop; + + const subscriptions = stateChangeEventKeys.map((et) => + channel.on(et, () => { + onStoreChange(selector(channel)); + }), + ); + + return () => subscriptions.forEach((subscription) => subscription.unsubscribe()); + }, + [channel, selector, stateChangeEventKeys], + ); + + const getSnapshot = useCallback(() => { + if (!channel) return undefined; + + return selector(channel); + }, [channel, selector]); + + return useSyncExternalStore(subscribe, getSnapshot); +} diff --git a/yarn.lock b/yarn.lock index 770e0ebbc..89b8b1937 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2769,6 +2769,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== + "@types/uuid@^8.3.0": version "8.3.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" @@ -12231,20 +12236,9 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -stream-chat@^8.50.0: - version "8.50.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.50.0.tgz#ae9bd40da3d38a0302d62d0165b2b8f45d500ae2" - integrity sha512-n7iAp0QTpZmRbygKFdwKlJy4QhhWep+6mQ+ctn2OjBEkrhtKITc4fIaMcQT4lOc7om0K9YTskMRvdWa2ZBvEWg== - dependencies: - "@babel/runtime" "^7.16.3" - "@types/jsonwebtoken" "~9.0.0" - "@types/ws" "^7.4.0" - axios "^1.6.0" - base64-js "^1.5.1" - form-data "^4.0.0" - isomorphic-ws "^4.0.1" - jsonwebtoken "~9.0.0" - ws "^7.5.10" +"stream-chat@link:../stream-chat-js": + version "0.0.0" + uid "" stream-combiner2@~1.1.1: version "1.1.1" @@ -13197,6 +13191,11 @@ use-latest@^1.0.0: dependencies: use-isomorphic-layout-effect "^1.0.0" +use-sync-external-store@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc" + integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"