Skip to content

Commit c2cb10b

Browse files
authored
docs(ai-migration): open the first channel once per load, not after every close (#3358)
Ticket: https://linear.app/stream/issue/REACT-1470 ### Goal Fix the v14 to v15 guide's "No channel opens on load" snippet (section 6, "v14 list defaults that changed"). The current snippet re-subscribes to the paginator whenever the open channels change, and `subscribeWithSelector` fires on subscribe, so it opens the first channel again as soon as the user closes the last one. An app restoring v14's `setActiveChannelOnMount` default wants it once per load. ### Implementation details Docs only. The snippet: - subscribes on `[paginator]` only and reads the navigation through a ref, so opening or closing a channel does not replay an already loaded page; - counts each load once: closing the last channel, or a channel moving to the top of the list, opens nothing; - re-arms when the list returns to "never queried" (`items === undefined`, where `client.disconnectUser()` resets it), so the next user's first load opens its first channel; - checks `openChannels` instead of a `useRef` flag, so a `ChatView` view switch that remounts it does not replace the channel the user picked. The bullet under the snippet now describes the once-per-load behavior. The snippet was checked by extracting it into a temporary hook and running a test suite against it (initial load, a page loaded before mount, remount after picking another channel, a `ChatView` threads/channels view switch, reorder, empty first page, reload after a reset, no provider): all 8 pass, and it passes ESLint. ### UI Changes None. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Documentation** * Updated the migration guide’s `useOpenFirstChannel` example to open the first channel only when no channel is already open, and to avoid overriding a channel selected by the user. * Added guidance on resetting this behavior when a list unloads, handling disconnect cleanup, and using the hook with multiple lists. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 290d2db commit c2cb10b

1 file changed

Lines changed: 33 additions & 16 deletions

File tree

‎ai-docs/ai-migration-v14-v15.md‎

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -988,28 +988,44 @@ export const createPaginator = (client: StreamChat, userId: string) =>
988988

989989
These hit apps that never passed the props, because they were v14 defaults:
990990

991-
- **No channel opens on load.** `setActiveChannelOnMount` defaulted to `true`, so every v14 app opened the first channel. v15 has no default selection: nothing is open until you call `useWorkspaceNavigation().openChannel(channel)`. To keep the v14 behavior, open the paginator's first item while no channel is open:
991+
- **No channel opens on load.** `setActiveChannelOnMount` defaulted to `true`, so every v14 app opened the first channel. v15 has no default selection: nothing is open until you call `useWorkspaceNavigation().openChannel(channel)`. To keep the v14 behavior, open the paginator's first item when its first page loads:
992992

993993
```tsx
994-
import { useEffect } from 'react';
994+
import { useEffect, useRef } from 'react';
995995
import type { ChannelPaginator } from 'stream-chat';
996996
import { ChannelNavigation, useWorkspaceNavigation } from 'stream-chat-react';
997997

998998
export const useOpenFirstChannel = (paginator: ChannelPaginator) => {
999999
const { openChannel, openChannels } = useWorkspaceNavigation();
1000-
// Guard on the navigation's state, not a ref: inside ChatView this hook remounts with its view,
1001-
// and a ref would reset and open the first channel over the one the user had open.
1002-
const nothingOpen = openChannels.length === 0;
1003-
useEffect(
1004-
() =>
1005-
paginator.state.subscribeWithSelector(
1006-
({ items }) => ({ first: items?.[0] }),
1007-
({ first }) => {
1008-
if (first && nothingOpen) openChannel(first);
1009-
},
1010-
),
1011-
[nothingOpen, openChannel, paginator],
1012-
);
1000+
// Read when a page loads. The navigation changes with every open and close, and resubscribing
1001+
// on it would replay a page that already loaded.
1002+
const navigation = useRef({ hasOpenChannel: openChannels.length > 0, openChannel });
1003+
useEffect(() => {
1004+
navigation.current = { hasOpenChannel: openChannels.length > 0, openChannel };
1005+
});
1006+
1007+
useEffect(() => {
1008+
// Each load counts once, so a channel that later moves to the top is not opened.
1009+
// `items === undefined` means "never queried", where `client.disconnectUser()` resets the
1010+
// list, so the next user's first load counts again.
1011+
let awaitingLoad = true;
1012+
return paginator.state.subscribeWithSelector(
1013+
({ items }) => ({ first: items?.[0], isUnloaded: items === undefined }),
1014+
({ first, isUnloaded }) => {
1015+
if (isUnloaded) {
1016+
awaitingLoad = true;
1017+
return;
1018+
}
1019+
if (!awaitingLoad) return;
1020+
awaitingLoad = false;
1021+
// Check what is open, not a flag of this hook: a remount (a `ChatView` view switch
1022+
// unmounts the list) must not replace the channel the user picked.
1023+
if (first && !navigation.current.hasOpenChannel) {
1024+
navigation.current.openChannel(first);
1025+
}
1026+
},
1027+
);
1028+
}, [paginator]);
10131029
};
10141030

10151031
// In `views.channels`; the paginator comes from useChannelPaginator, above ChatView.
@@ -1021,7 +1037,8 @@ These hit apps that never passed the props, because they were v14 defaults:
10211037

10221038
- Call it below the navigation provider (inside `ChatView`, or inside your `WorkspaceNavigationProvider`), where `openChannel` does something.
10231039
- Inside `ChatView`, call it in the channels view, not in an always-rendered `ChatView` child. `openChannels` lists the active view's channels only, so it is empty in the threads view, and opening a channel from there switches the user back to the channels view.
1024-
- It opens the first channel again whenever nothing is open, for example after your own navigation closes the last channel. If an empty selection is a valid state in your app, keep an "already opened" flag above `ChatView` instead.
1040+
- It opens once per load of the list: closing the last channel, or a channel moving to the top, opens nothing. After `client.disconnectUser()` resets the list, the next load opens its first channel again, once the previous user's channel is closed: inside `ChatView`, mount `<DisposedChannelRelease />` (from `stream-chat-react/slot-layout`); without it, clear your own state as described in [Without `ChatView`](#without-chatview).
1041+
- With several lists, call it once, for the list whose first channel should open. Each call knows only its own paginator, so with one call per list the first page to arrive wins, and two pages arriving before a re-render can both open.
10251042
- Create the paginator above `ChatView` too ([above](#channellist-is-driven-by-a-channelpaginator)); one created inside the view is replaced on every switch, and the hook then subscribes to a fresh, unloaded paginator.
10261043

10271044
- **Unfiltered channels no longer join the list.** `allowNewMessagesFromUnfilteredChannels` defaulted to `true`: a new message in a channel that was not in the list added it, whatever the list's filters. In v15 the paginator adds a channel only if it matches the paginator's own filters, and there is no opt-out. If channels used to appear this way, widen the filters to cover them.

0 commit comments

Comments
 (0)