Skip to content

Commit e123c3c

Browse files
authored
feat: add channel list context (v10) (#2195)
1 parent 2932ba1 commit e123c3c

File tree

5 files changed

+224
-4
lines changed

5 files changed

+224
-4
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
---
2+
id: channel_list_context
3+
sidebar_position: 11
4+
title: ChannelListContext
5+
---
6+
7+
The context value is provided by `ChannelListContextProvider` which wraps the contents rendered by [`ChannelList`](../core-components/channel-list.mdx). It exposes API that the default and custom components rendered by `ChannelList` can take advantage of. The components that can consume the context are customizable via `ChannelListProps`:
8+
9+
- `Avatar` - component used to display channel image
10+
- `ChannelSearch` - renders channel search input and results
11+
- `EmptyStateIndicator` - rendered when the channels query returns and empty array
12+
- `LoadingErrorIndicator` - rendered when the channels query fails
13+
- `LoadingIndicator`- rendered during the channels query
14+
- `List` - component rendering `LoadingErrorIndicator`, `LoadingIndicator`, `EmptyStateIndicator`, `Paginator` and the list of channel `Preview` components
15+
- `Paginator` - takes care of requesting to load more channels into the list (pagination)
16+
- `Preview` - renders the information of a channel in the channel list
17+
18+
## Basic Usage
19+
20+
Access the API from context with our custom hook:
21+
22+
```jsx
23+
import { useChannelListContext } from 'stream-chat-react';
24+
25+
export const CustomComponent = () => {
26+
const { channels, setChannels } = useChannelListContext();
27+
// component logic ...
28+
return(
29+
{/* rendered elements */}
30+
);
31+
}
32+
```
33+
34+
## Value
35+
36+
### channels
37+
38+
State representing the array of loaded channels. Channels query is executed by default only within the [`ChannelList` component](../core-components/channel-list.mdx) in the SDK.
39+
40+
| Type |
41+
|-------------|
42+
| `Channel[]` |
43+
44+
### setChannels
45+
46+
Sets the list of `Channel` objects to be rendered by `ChannelList` component. One have to be careful, when to call `setChannels` as the first channels query executed by the `ChannelList` overrides the whole [`channels` state](#channels). In that case it is better to subscribe to `client` event `channels.queried` and only then set the channels.
47+
In the following example, we have a component that sets the active channel based on the id in the URL. It waits until the first channels page is loaded, and then it sets the active channel. If the channel is not present on the first page, it performs additional API request with `getChannel()`:
48+
49+
```tsx
50+
import { useEffect } from 'react';
51+
import { useNavigate, useParams } from 'react-router-dom';
52+
import { ChannelList, ChannelListMessenger, ChannelListMessengerProps, getChannel, useChannelListContext, useChatContext } from 'stream-chat-react';
53+
54+
const DEFAULT_CHANNEL_ID = 'general';
55+
const DEFAULT_CHANNEL_TYPE = 'messaging';
56+
57+
const List = (props: ChannelListMessengerProps) => {
58+
const { channelId } = useParams();
59+
const navigate = useNavigate();
60+
const { client, channel, setActiveChannel } = useChatContext();
61+
const { setChannels } = useChannelListContext();
62+
63+
useEffect(() => {
64+
if (!channelId) return navigate(`/${DEFAULT_CHANNEL_ID}`);
65+
66+
if (channel?.id === channelId || !client) return;
67+
68+
let subscription: { unsubscribe: () => void } | undefined;
69+
if(!channel?.id || channel?.id !== channelId) {
70+
subscription = client.on('channels.queried', (event: Event) => {
71+
const loadedChannelData = event.queriedChannels?.channels.find((response) => response.channel.id === channelId);
72+
73+
if (loadedChannelData) {
74+
setActiveChannel(client.channel( DEFAULT_CHANNEL_TYPE, channelId));
75+
subscription?.unsubscribe();
76+
return;
77+
}
78+
79+
return getChannel({client, id: channelId, type: DEFAULT_CHANNEL_TYPE}).then((newActiveChannel) => {
80+
setActiveChannel(newActiveChannel);
81+
setChannels((channels) => {
82+
return ([newActiveChannel, ...channels.filter((ch) => ch.data?.cid !== newActiveChannel.data?.cid)]);
83+
});
84+
});
85+
});
86+
}
87+
88+
return () => {
89+
subscription?.unsubscribe();
90+
};
91+
}, [channel?.id, channelId, setChannels, client, navigate, setActiveChannel]);
92+
93+
return <ChannelListMessenger {...props}/>;
94+
};
95+
96+
97+
98+
const Sidebar = () => {
99+
return (
100+
// ...
101+
<ChannelList
102+
{/* some props */}
103+
{/* setting active channel will be performed inside the custom List component */}
104+
setActiveChannelOnMount={false}
105+
List={List}
106+
{/* some props */}
107+
/>
108+
// ...
109+
}
110+
```
111+
112+
| Type |
113+
|---------------------------------------|
114+
| `Dispatch<SetStateAction<Channel[]>>` |

src/components/ChannelList/ChannelList.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
import { LoadingChannels } from '../Loading/LoadingChannels';
3232
import { LoadMorePaginator, LoadMorePaginatorProps } from '../LoadMore/LoadMorePaginator';
3333

34+
import { ChannelListContextProvider } from '../../context';
3435
import { useChatContext } from '../../context/ChatContext';
3536

3637
import type { Channel, ChannelFilters, ChannelOptions, ChannelSort, Event } from 'stream-chat';
@@ -336,7 +337,7 @@ const UnMemoizedChannelList = <
336337

337338
const showChannelList = !searchActive || additionalChannelSearchProps?.popupResults;
338339
return (
339-
<>
340+
<ChannelListContextProvider value={{ channels, setChannels }}>
340341
<div className={className} ref={channelListRef}>
341342
{showChannelSearch && (
342343
<ChannelSearch
@@ -374,7 +375,7 @@ const UnMemoizedChannelList = <
374375
</List>
375376
)}
376377
</div>
377-
</>
378+
</ChannelListContextProvider>
378379
);
379380
};
380381

src/components/ChannelList/__tests__/ChannelList.test.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import { nanoid } from 'nanoid';
33
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
44
import '@testing-library/jest-dom';
@@ -36,7 +36,7 @@ import {
3636
ChannelPreviewMessenger,
3737
} from '../../ChannelPreview';
3838

39-
import { ChatContext, useChatContext } from '../../../context/ChatContext';
39+
import { ChatContext, useChannelListContext, useChatContext } from '../../../context';
4040
import { ChannelListMessenger } from '../ChannelListMessenger';
4141

4242
expect.extend(toHaveNoViolations);
@@ -1663,4 +1663,47 @@ describe('ChannelList', () => {
16631663
dateNowSpy.mockRestore();
16641664
});
16651665
});
1666+
1667+
describe('context', () => {
1668+
it('allows to set the new list of channels', async () => {
1669+
let setChannelsFromOutside;
1670+
const channelsToBeLoaded = Array.from({ length: 5 }, generateChannel);
1671+
const channelsToBeSet = Array.from({ length: 5 }, generateChannel);
1672+
const channelsToIdString = (channels) => channels.map(({ id }) => id).join();
1673+
const channelsDataToIdString = (channels) => channels.map(({ channel: { id } }) => id).join();
1674+
1675+
const ChannelListCustom = () => {
1676+
const { channels, setChannels } = useChannelListContext();
1677+
useEffect(() => {
1678+
setChannelsFromOutside = setChannels;
1679+
}, []);
1680+
return <div>{channelsToIdString(channels)}</div>;
1681+
};
1682+
const props = {
1683+
filters: {},
1684+
List: ChannelListCustom,
1685+
Preview: ChannelPreviewComponent,
1686+
};
1687+
1688+
useMockedApis(chatClient, [queryChannelsApi(channelsToBeLoaded)]);
1689+
1690+
await act(async () => {
1691+
await render(
1692+
<Chat client={chatClient}>
1693+
<ChannelList {...props} />
1694+
</Chat>,
1695+
);
1696+
});
1697+
1698+
expect(screen.getByText(channelsDataToIdString(channelsToBeLoaded))).toBeInTheDocument();
1699+
1700+
await act(() => {
1701+
setChannelsFromOutside(chatClient.hydrateActiveChannels(channelsToBeSet));
1702+
});
1703+
expect(
1704+
screen.queryByText(channelsDataToIdString(channelsToBeLoaded)),
1705+
).not.toBeInTheDocument();
1706+
expect(screen.getByText(channelsDataToIdString(channelsToBeSet))).toBeInTheDocument();
1707+
});
1708+
});
16661709
});

src/context/ChannelListContext.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, {
2+
createContext,
3+
Dispatch,
4+
PropsWithChildren,
5+
SetStateAction,
6+
useContext,
7+
} from 'react';
8+
9+
import type { Channel } from 'stream-chat';
10+
11+
import type { DefaultStreamChatGenerics } from '../types/types';
12+
13+
export type ChannelListContextValue<
14+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
15+
> = {
16+
/**
17+
* State representing the array of loaded channels.
18+
* Channels query is executed by default only by ChannelList component in the SDK.
19+
*/
20+
channels: Channel<StreamChatGenerics>[];
21+
/**
22+
* Sets the list of Channel objects to be rendered by ChannelList component.
23+
*/
24+
setChannels: Dispatch<SetStateAction<Channel<StreamChatGenerics>[]>>;
25+
};
26+
27+
export const ChannelListContext = createContext<ChannelListContextValue | undefined>(undefined);
28+
29+
/**
30+
* Context provider for components rendered within the `ChannelList`
31+
*/
32+
export const ChannelListContextProvider = <
33+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
34+
>({
35+
children,
36+
value,
37+
}: PropsWithChildren<{
38+
value: ChannelListContextValue<StreamChatGenerics>;
39+
}>) => (
40+
<ChannelListContext.Provider value={(value as unknown) as ChannelListContextValue}>
41+
{children}
42+
</ChannelListContext.Provider>
43+
);
44+
45+
export const useChannelListContext = <
46+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
47+
>(
48+
componentName?: string,
49+
) => {
50+
const contextValue = useContext(ChannelListContext);
51+
52+
if (!contextValue) {
53+
console.warn(
54+
`The useChannelListContext hook was called outside of the ChannelListContext provider. Make sure this hook is called within the ChannelList component. The errored call is located in the ${componentName} component.`,
55+
);
56+
57+
return {} as ChannelListContextValue<StreamChatGenerics>;
58+
}
59+
60+
return (contextValue as unknown) as ChannelListContextValue<StreamChatGenerics>;
61+
};

src/context/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './ChannelActionContext';
2+
export * from './ChannelListContext';
23
export * from './ChannelStateContext';
34
export * from './ChatContext';
45
export * from './ComponentContext';

0 commit comments

Comments
 (0)