From 19d8fda7ee17ad9b19c80d3aa6d2e3ca13c5d9b8 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:57:09 +0100 Subject: [PATCH] fix: state store with new api (#2726) * fix: state store declaration in line with new api * fix: lint issues * fix: SampleApp as well * fix: unread count badge as well * chore: update docs with api changes * chore: bump stream-chat version in sdk * fix: lint issues with docs * fix: revert SampleApp changes as sdk is not released * fix: remove unwaranted claims from docs --- .../state-overview.mdx | 39 +++++++++++-------- package/package.json | 2 +- .../Channel/hooks/useCreateThreadContext.ts | 15 ++++--- .../src/components/ThreadList/ThreadList.tsx | 8 +++- .../components/ThreadList/ThreadListItem.tsx | 17 ++++---- .../ThreadList/ThreadListUnreadBanner.tsx | 5 ++- package/src/hooks/useStateStore.ts | 24 ++++++------ package/yarn.lock | 8 ++-- 8 files changed, 64 insertions(+), 54 deletions(-) diff --git a/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx b/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx index f13f4f64da..51474fc6f3 100644 --- a/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx +++ b/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx @@ -266,40 +266,45 @@ Selectors are functions provided by integrators that run whenever state object c #### Rules of Selectors -1. Selectors should return array of data sorted by their "change factor"; meaning values that change often should come first for the best performance. +1. Selectors should return a named object. ```ts -const selector = (nextValue: ThreadManagerState) => [ - nextValue.unreadThreadsCount, // <-- changes often - nextValue.active, // <-- changes less often - nextvalue.lastConnectionDownAt, // <-- changes rarely -]; +const selector = (nextValue: ThreadManagerState) => ({ + unreadThreadsCount: nextValue.unreadThreadsCount, + active: nextValue.active, + lastConnectionDownAt: nextvalue.lastConnectionDownAt, +}); ``` -2. Selectors should live outside components scope or should be memoized if it requires "outside" information (`userId` for `read` object for example). Not memoizing selectors (or not stabilizing them) will lead to bad performance as each time your component re-renders, the selector function is created anew and `useSimpleStateStore` goes through unsubscribe and resubscribe process unnecessarily. +2. Selectors should live outside components scope or should be memoized if it requires "outside" information (`userId` for `read` object for example). Not memoizing selectors (or not stabilizing them) will lead to bad performance as each time your component re-renders, the selector function is created anew and `useStateStore` goes through unsubscribe and resubscribe process unnecessarily. ```tsx // ❌ not okay const Component1 = () => { - const [latestReply] = useStateStore(thread.state, (nextValue: ThreadState) => [nextValue.latestReplies.at(-1)]); + const { latestReply } = useStateStore(thread.state, (nextValue: ThreadState) => ({ + latestReply: nextValue.latestReplies.at(-1), + })); return {latestReply.text}; }; // ✅ okay -const selector = (nextValue: ThreadState) => [nextValue.latestReplies.at(-1)]; +const selector = (nextValue: ThreadState) => ({ latestReply: nextValue.latestReplies.at(-1) }); const Component2 = () => { - const [latestReply] = useStateStore(thread.state, selector); + const { latestReply } = useStateStore(thread.state, selector); return {latestReply.text}; }; // ✅ also okay const Component3 = ({ userId }: { userId: string }) => { - const selector = useCallback((nextValue: ThreadState) => [nextValue.read[userId].unread_messages], [userId]); + const selector = useCallback( + (nextValue: ThreadState) => ({ unreadMessagesCount: nextValue.read[userId].unread_messages }), + [userId], + ); - const [unreadMessagesCount] = useStateStore(thread.state, selector); + const { unreadMessagesCount } = useStateStore(thread.state, selector); return {unreadMessagesCount}; }; @@ -324,9 +329,9 @@ client.threads.state.subscribe(console.log); let latestThreads; client.threads.state.subscribeWithSelector( // called each time theres a change in the state object - nextValue => [nextValue.threads], + nextValue => ({ threads: nextValue.threads }), // called only when threads change (selected value) - ([threads]) => { + ({ threads }) => { latestThreads = threads; }, ); @@ -344,17 +349,17 @@ thread?.state.getLatestValue(/*...*/); #### useStateStore Hook -For the ease of use - the React SDK comes with the appropriate state access hook which wraps `SimpleStateStore.subscribeWithSelector` API for the React-based applications. +For the ease of use - the React SDK comes with the appropriate state access hook which wraps `StateStore.subscribeWithSelector` API for the React-based applications. ```tsx import { useStateStore } from 'stream-chat-react-native'; import type { ThreadManagerState } from 'stream-chat'; -const selector = (nextValue: ThreadManagerState) => [nextValue.threads] as const; +const selector = (nextValue: ThreadManagerState) => ({ threads: nextValue.threads }) as const; const CustomThreadList = () => { const { client } = useChatContext(); - const [threads] = useStateStore(client.threads.state, selector); + const { threads } = useStateStore(client.threads.state, selector); return ( diff --git a/package/package.json b/package/package.json index 2b7a8e64e1..ec2cef2103 100644 --- a/package/package.json +++ b/package/package.json @@ -78,7 +78,7 @@ "path": "0.12.7", "react-native-markdown-package": "1.8.2", "react-native-url-polyfill": "^1.3.0", - "stream-chat": "8.40.8" + "stream-chat": "8.41.1" }, "peerDependencies": { "react-native-quick-sqlite": ">=5.1.0", diff --git a/package/src/components/Channel/hooks/useCreateThreadContext.ts b/package/src/components/Channel/hooks/useCreateThreadContext.ts index 08653a9dfa..e469c9a909 100644 --- a/package/src/components/Channel/hooks/useCreateThreadContext.ts +++ b/package/src/components/Channel/hooks/useCreateThreadContext.ts @@ -5,12 +5,11 @@ import { useStateStore } from '../../../hooks'; import type { DefaultStreamChatGenerics } from '../../../types/types'; const selector = (nextValue: ThreadState) => - [ - nextValue.replies, - nextValue.pagination.isLoadingPrev, - nextValue.pagination.isLoadingNext, - nextValue.parentMessage, - ] as const; + ({ + isLoadingNext: nextValue.pagination.isLoadingNext, + isLoadingPrev: nextValue.pagination.isLoadingPrev, + latestReplies: nextValue.replies, + } as const); export const useCreateThreadContext = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -27,8 +26,8 @@ export const useCreateThreadContext = < threadLoadingMore, threadMessages, }: ThreadContextValue) => { - const [latestReplies, isLoadingPrev, isLoadingNext] = - useStateStore(threadInstance?.state, selector) ?? []; + const { isLoadingNext, isLoadingPrev, latestReplies } = + useStateStore(threadInstance?.state, selector) ?? {}; const contextAdapter = threadInstance ? { diff --git a/package/src/components/ThreadList/ThreadList.tsx b/package/src/components/ThreadList/ThreadList.tsx index e0fb8eb391..003b054ff5 100644 --- a/package/src/components/ThreadList/ThreadList.tsx +++ b/package/src/components/ThreadList/ThreadList.tsx @@ -18,7 +18,11 @@ import { EmptyStateIndicator } from '../Indicators/EmptyStateIndicator'; import { LoadingIndicator } from '../Indicators/LoadingIndicator'; const selector = (nextValue: ThreadManagerState) => - [nextValue.threads, nextValue.pagination.isLoading, nextValue.pagination.isLoadingNext] as const; + ({ + isLoading: nextValue.pagination.isLoading, + isLoadingNext: nextValue.pagination.isLoadingNext, + threads: nextValue.threads, + } as const); export type ThreadListProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -107,7 +111,7 @@ export const ThreadList = (props: ThreadListProps) => { }; }, [client]); - const [threads, isLoading, isLoadingNext] = useStateStore(client.threads.state, selector); + const { isLoading, isLoadingNext, threads } = useStateStore(client.threads.state, selector); return ( { const selector = useCallback( (nextValue: ThreadState) => - [ - nextValue.replies.at(-1), - (client.userID && nextValue.read[client.userID]?.unreadMessageCount) || 0, - nextValue.parentMessage, - nextValue.channel, - nextValue.deletedAt, - ] as const, + ({ + channel: nextValue.channel, + deletedAt: nextValue.deletedAt, + lastReply: nextValue.replies.at(-1), + ownUnreadMessageCount: + (client.userID && nextValue.read[client.userID]?.unreadMessageCount) || 0, + parentMessage: nextValue.parentMessage, + } as const), [client], ); - const [lastReply, ownUnreadMessageCount, parentMessage, channel, deletedAt] = useStateStore( + const { channel, deletedAt, lastReply, ownUnreadMessageCount, parentMessage } = useStateStore( thread.state, selector, ); diff --git a/package/src/components/ThreadList/ThreadListUnreadBanner.tsx b/package/src/components/ThreadList/ThreadListUnreadBanner.tsx index 10b2c41f27..98f08c608f 100644 --- a/package/src/components/ThreadList/ThreadListUnreadBanner.tsx +++ b/package/src/components/ThreadList/ThreadListUnreadBanner.tsx @@ -19,7 +19,8 @@ const styles = StyleSheet.create({ }, }); -const selector = (nextValue: ThreadManagerState) => [nextValue.unseenThreadIds] as const; +const selector = (nextValue: ThreadManagerState) => + ({ unseenThreadIds: nextValue.unseenThreadIds } as const); export const ThreadListUnreadBanner = () => { const { client } = useChatContext(); @@ -29,7 +30,7 @@ export const ThreadListUnreadBanner = () => { threadListUnreadBanner, }, } = useTheme(); - const [unseenThreadIds] = useStateStore(client.threads.state, selector); + const { unseenThreadIds } = useStateStore(client.threads.state, selector); if (!unseenThreadIds.length) { return null; } diff --git a/package/src/hooks/useStateStore.ts b/package/src/hooks/useStateStore.ts index fbc0cfe3c6..a74bfe9d1f 100644 --- a/package/src/hooks/useStateStore.ts +++ b/package/src/hooks/useStateStore.ts @@ -2,18 +2,18 @@ import { useEffect, useState } from 'react'; import type { StateStore } from 'stream-chat'; -export function useStateStore, O extends readonly unknown[]>( - store: StateStore, - selector: (v: T) => O, -): O; -export function useStateStore, O extends readonly unknown[]>( - store: StateStore | undefined, - selector: (v: T) => O, -): O | undefined; -export function useStateStore, O extends readonly unknown[]>( - store: StateStore | undefined, - selector: (v: T) => O, -) { +export function useStateStore< + T extends Record, + O extends Readonly | Readonly>, +>(store: StateStore, selector: (v: T) => O): O; +export function useStateStore< + T extends Record, + O extends Readonly | Readonly>, +>(store: StateStore | undefined, selector: (v: T) => O): O | undefined; +export function useStateStore< + T extends Record, + O extends Readonly | Readonly>, +>(store: StateStore | undefined, selector: (v: T) => O) { const [state, setState] = useState(() => { if (!store) return undefined; return selector(store.getLatestValue()); diff --git a/package/yarn.lock b/package/yarn.lock index 5f0842308c..3d9121a887 100644 --- a/package/yarn.lock +++ b/package/yarn.lock @@ -10664,10 +10664,10 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-chat@8.40.8: - version "8.40.8" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.40.8.tgz#0f5320bd8b03d1cbff377f8c7ae2f8afe24d0515" - integrity sha512-nYLvYAkrvXRzuPO52TIofNiInCkDdXrnBc/658297lC6hzrHNc87mmTht264BXmXLlpasTNP3rLKxR6MxhpgKg== +stream-chat@8.41.1: + version "8.41.1" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.41.1.tgz#c991980b800b67ec38202a1aa3bbbd4112ccb5fa" + integrity sha512-WV0mHHm88D4RbAV42sD0+SqTWLCvjIwfGZ3nSBXRAuGpVYJEqnNUhEd4OIQ+YrXVbjY7qWz9L5XRk5fZIfE9kg== dependencies: "@babel/runtime" "^7.16.3" "@types/jsonwebtoken" "~9.0.0"