Skip to content

fix: state store with new api #2726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Text>{latestReply.text}</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 <Text>{latestReply.text}</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 <Text>{unreadMessagesCount}</Text>;
};
Expand All @@ -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;
},
);
Expand All @@ -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 (
<View>
Expand Down
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 7 additions & 8 deletions package/src/components/Channel/hooks/useCreateThreadContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,8 +26,8 @@ export const useCreateThreadContext = <
threadLoadingMore,
threadMessages,
}: ThreadContextValue<StreamChatGenerics>) => {
const [latestReplies, isLoadingPrev, isLoadingNext] =
useStateStore(threadInstance?.state, selector) ?? [];
const { isLoadingNext, isLoadingPrev, latestReplies } =
useStateStore(threadInstance?.state, selector) ?? {};

const contextAdapter = threadInstance
? {
Expand Down
8 changes: 6 additions & 2 deletions package/src/components/ThreadList/ThreadList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<ThreadsProvider
Expand Down
17 changes: 9 additions & 8 deletions package/src/components/ThreadList/ThreadListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,18 @@ export const ThreadListItem = (props: ThreadListItemProps) => {

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,
);
Expand Down
5 changes: 3 additions & 2 deletions package/src/components/ThreadList/ThreadListUnreadBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}
Expand Down
24 changes: 12 additions & 12 deletions package/src/hooks/useStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { useEffect, useState } from 'react';

import type { StateStore } from 'stream-chat';

export function useStateStore<T extends Record<string, unknown>, O extends readonly unknown[]>(
store: StateStore<T>,
selector: (v: T) => O,
): O;
export function useStateStore<T extends Record<string, unknown>, O extends readonly unknown[]>(
store: StateStore<T> | undefined,
selector: (v: T) => O,
): O | undefined;
export function useStateStore<T extends Record<string, unknown>, O extends readonly unknown[]>(
store: StateStore<T> | undefined,
selector: (v: T) => O,
) {
export function useStateStore<
T extends Record<string, unknown>,
O extends Readonly<Record<string, unknown> | Readonly<unknown[]>>,
>(store: StateStore<T>, selector: (v: T) => O): O;
export function useStateStore<
T extends Record<string, unknown>,
O extends Readonly<Record<string, unknown> | Readonly<unknown[]>>,
>(store: StateStore<T> | undefined, selector: (v: T) => O): O | undefined;
export function useStateStore<
T extends Record<string, unknown>,
O extends Readonly<Record<string, unknown> | Readonly<unknown[]>>,
>(store: StateStore<T> | undefined, selector: (v: T) => O) {
const [state, setState] = useState<O | undefined>(() => {
if (!store) return undefined;
return selector(store.getLatestValue());
Expand Down
8 changes: 4 additions & 4 deletions package/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading