Skip to content

Commit

Permalink
fix: state store with new api (#2726)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
isekovanic authored Oct 28, 2024
1 parent c72efaf commit 19d8fda
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 54 deletions.
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

0 comments on commit 19d8fda

Please sign in to comment.