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"