Skip to content

Commit 15dd5e1

Browse files
refactor: introduce WithComponents context provider (#3542)
## 🎯 Goal Replace the prop-drilling of 120+ component overrides with a single `WithComponents` context provider. This eliminates massive boilerplate across `Channel`, `ChannelList`, and all consumer components. **Before:** ```tsx <Channel Message={MyMessage} SendButton={MySendButton} Reply={MyReply} channel={channel}> ``` **After:** ```tsx <WithComponents overrides={{ Message: MyMessage, SendButton: MySendButton, Reply: MyReply }}> <Channel channel={channel}> </WithComponents> ``` ## 🛠 Implementation details ### Design principle **All components are read from `useComponentsContext()`. All other contexts only provide data + APIs — never components.** ### New `ComponentsContext` - `WithComponents` provider — nestable, inner overrides win. Uses `overrides` prop (aligned with `stream-chat-react`) - `useComponentsContext()` hook — returns all components with defaults filled in - `ComponentOverrides` type — auto-derived from `DEFAULT_COMPONENTS` map (`Partial<typeof DEFAULT_COMPONENTS>`) - `defaultComponents.ts` — single source of truth for all ~120 default component mappings (lazy-loaded to avoid circular deps) - Adding a new overridable component only requires editing `defaultComponents.ts` — the type is derived automatically ### What changed - **Stripped component keys** from `MessagesContextValue`, `InputMessageInputContextValue`, `ChannelContextValue`, `ChannelsContextValue`, `AttachmentPickerContextValue`, `ThreadsContextValue`, `ImageGalleryContextValue` — these now only carry data + APIs - **Simplified `useCreate*Context` hooks** — no longer receive or forward component params - **Simplified `Channel.tsx`** — removed ~90 component imports, prop defaults, and forwarding lines - **Simplified `ChannelList.tsx`** — removed ~19 component props - **Updated ~80 consumer files** — switched from old context hooks to `useComponentsContext()` for component reads - **Removed component override props** from ALL individual components (Chat, Thread, ThreadList, Poll, ChannelPreview, ImageGallery, etc.) - **No component accepts component overrides as props anymore** — everything goes through `WithComponents` - **`ComponentsContext.tsx` reduced from ~350 lines to ~55 lines** — type is derived from defaults, no manual type maintenance - **Updated all 3 example apps** (SampleApp, ExpoMessaging, TypeScriptMessaging) to use `WithComponents` ### Net result - **90+ files changed**, net **-2500+ lines** removed - Each component override name went from being listed 4 times to 0 in the forwarding pipeline - One place to override components: `<WithComponents overrides={{ ... }}>` - One place to read components: `useComponentsContext()` - One place to add new overridable components: `defaultComponents.ts` ## BREAKING CHANGE - **Component override props removed from all SDK components** — `<Channel Message={X}>`, `<ChannelList Preview={X}>`, `<Thread MessageComposer={X}>`, etc. no longer accept component overrides as props. Use `<WithComponents overrides={{ Message: X }}>` instead. - **Component keys removed from context value types** — `MessagesContextValue`, `InputMessageInputContextValue`, `ChannelContextValue`, `ChannelsContextValue`, `AttachmentPickerContextValue`, `ThreadsContextValue`, `ImageGalleryContextValue` no longer include component-type keys. Use `useComponentsContext()` to read component overrides. - **`List` prop removed from `ChannelList`** — use custom `EmptyStateIndicator` override or wrap `ChannelListView` directly. - **`LoadingIndicator` prop removed from `Chat`** — use `<WithComponents overrides={{ ChatLoadingIndicator: X }}>`. ## 🎨 UI Changes No visual changes — this is a pure structural refactor. ## 🧪 Testing - `yarn build` — 0 type errors - `yarn lint` — passes clean (0 warnings, 0 errors) - `yarn test:unit` — 91/92 suites pass; 1 pre-existing timeout failure in `offline-support` that also fails on `develop` - New test: `defaultComponents.test.ts` verifies all default component mappings are defined and optional keys are explicitly listed - Updated test files to use `<WithComponents overrides={...}>` wrapper instead of removed component override props ## ☑️ Checklist - [x] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [x] PR targets the `develop` branch - [ ] Documentation is updated - [x] New code is tested in main example apps, including all possible scenarios - [x] SampleApp iOS and Android - [x] Expo iOS and Android --------- Co-authored-by: Ivan Sekovanikj <ivan.sekovanikj@getstream.io>
1 parent 482b782 commit 15dd5e1

111 files changed

Lines changed: 1705 additions & 3203 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/ExpoMessaging/app/channel/[cid]/index.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
useChatContext,
77
ThreadContextValue,
88
MessageList,
9+
WithComponents,
910
} from 'stream-chat-expo';
1011
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
1112
import { AuthProgressLoader } from '../../../components/AuthProgressLoader';
@@ -70,22 +71,23 @@ export default function ChannelScreen() {
7071
<Stack.Screen
7172
options={{ title: 'Channel Screen', contentStyle: { backgroundColor: 'white' } }}
7273
/>
73-
<Channel
74-
audioRecordingEnabled={true}
75-
channel={channel}
76-
onPressMessage={onPressMessage}
77-
keyboardVerticalOffset={headerHeight}
78-
MessageLocation={MessageLocation}
79-
thread={thread}
80-
>
81-
<MessageList
82-
onThreadSelect={(thread: ThreadContextValue['thread']) => {
83-
setThread(thread);
84-
router.push(`/channel/${channel.cid}/thread/${thread?.cid ?? ''}`);
85-
}}
86-
/>
87-
<MessageComposer InputButtons={InputButtons} />
88-
</Channel>
74+
<WithComponents overrides={{ MessageLocation, InputButtons }}>
75+
<Channel
76+
audioRecordingEnabled={true}
77+
channel={channel}
78+
onPressMessage={onPressMessage}
79+
keyboardVerticalOffset={headerHeight}
80+
thread={thread}
81+
>
82+
<MessageList
83+
onThreadSelect={(thread: ThreadContextValue['thread']) => {
84+
setThread(thread);
85+
router.push(`/channel/${channel.cid}/thread/${thread?.cid ?? ''}`);
86+
}}
87+
/>
88+
<MessageComposer />
89+
</Channel>
90+
</WithComponents>
8991
</View>
9092
);
9193
}

examples/ExpoMessaging/components/InputButtons.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React, { useState } from 'react';
22
import { Pressable, StyleSheet } from 'react-native';
3-
import { Channel, InputButtons as DefaultInputButtons } from 'stream-chat-expo';
3+
import type { ComponentOverrides } from 'stream-chat-expo';
4+
import { InputButtons as DefaultInputButtons } from 'stream-chat-expo';
45
import { ShareLocationIcon } from '../icons/ShareLocationIcon';
56
import { LiveLocationCreateModal } from './LocationSharing/CreateLocationModal';
67

7-
const InputButtons: NonNullable<React.ComponentProps<typeof Channel>['InputButtons']> = (props) => {
8+
const InputButtons: NonNullable<ComponentOverrides['InputButtons']> = (props) => {
89
const [modalVisible, setModalVisible] = useState(false);
910

1011
const onRequestClose = () => {

examples/SampleApp/src/screens/ChannelListScreen.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,7 @@ import {
1010
View,
1111
} from 'react-native';
1212
import { useNavigation, useScrollToTop } from '@react-navigation/native';
13-
import {
14-
ChannelList,
15-
useTheme,
16-
useStableCallback,
17-
ChannelActionItem,
18-
} from 'stream-chat-react-native';
13+
import { ChannelList, useTheme, useStableCallback, ChannelActionItem, WithComponents } from 'stream-chat-react-native';
1914
import { Channel } from 'stream-chat';
2015
import { ChannelPreview } from '../components/ChannelPreview';
2116
import { ChatScreenHeader } from '../components/ChatScreenHeader';
@@ -254,18 +249,23 @@ export const ChannelListScreen: React.FC = () => {
254249
)}
255250
<View style={{ flex: searchQuery ? 0 : 1 }}>
256251
<View style={[styles.channelListContainer, { opacity: searchQuery ? 0 : 1 }]}>
257-
<ChannelList
258-
additionalFlatListProps={additionalFlatListProps}
259-
filters={filters}
260-
HeaderNetworkDownIndicator={HeaderNetworkDownIndicator}
261-
maxUnreadCount={99}
262-
onSelect={onSelect}
263-
options={options}
264-
Preview={ChannelPreview}
265-
setFlatListRef={setScrollRef}
266-
getChannelActionItems={getChannelActionItems}
267-
sort={sort}
268-
/>
252+
<WithComponents
253+
overrides={{
254+
HeaderNetworkDownIndicator,
255+
Preview: ChannelPreview,
256+
}}
257+
>
258+
<ChannelList
259+
additionalFlatListProps={additionalFlatListProps}
260+
filters={filters}
261+
maxUnreadCount={99}
262+
onSelect={onSelect}
263+
options={options}
264+
setFlatListRef={setScrollRef}
265+
getChannelActionItems={getChannelActionItems}
266+
sort={sort}
267+
/>
268+
</WithComponents>
269269
</View>
270270
</View>
271271
</View>

examples/SampleApp/src/screens/ChannelScreen.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
MessageActionsParams,
1818
ChannelAvatar,
1919
PortalWhileClosingView,
20+
WithComponents,
2021
} from 'stream-chat-react-native';
2122
import { Pressable, StyleSheet, View } from 'react-native';
2223
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
@@ -265,19 +266,23 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({ navigation, route
265266

266267
return (
267268
<View style={[styles.flex, { backgroundColor: 'transparent' }]}>
269+
<WithComponents
270+
overrides={{
271+
// AttachmentPickerSelectionBar: CustomAttachmentPickerSelectionBar,
272+
AttachmentPickerContent: CustomAttachmentPickerContent,
273+
MessageLocation,
274+
NetworkDownIndicator: () => null,
275+
}}
276+
>
268277
<Channel
269278
audioRecordingEnabled={true}
270-
// AttachmentPickerSelectionBar={CustomAttachmentPickerSelectionBar}
271-
AttachmentPickerContent={CustomAttachmentPickerContent}
272279
channel={channel}
273280
messageInputFloating={messageInputFloating}
274281
onPressMessage={onPressMessage}
275282
initialScrollToFirstUnreadMessage
276283
keyboardVerticalOffset={0}
277284
messageActions={messageActions}
278-
MessageLocation={MessageLocation}
279285
messageId={messageId}
280-
NetworkDownIndicator={() => null}
281286
onAlsoSentToChannelHeaderPress={onAlsoSentToChannelHeaderPress}
282287
thread={selectedThread}
283288
maximumMessageLimit={messageListPruning}
@@ -306,6 +311,7 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({ navigation, route
306311
/>
307312
)}
308313
</Channel>
314+
</WithComponents>
309315
</View>
310316
);
311317
};

examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
MessageList,
99
UserAdd,
1010
useTheme,
11+
WithComponents,
1112
} from 'stream-chat-react-native';
1213

1314
import { User } from '../icons/User';
@@ -338,32 +339,37 @@ export const NewDirectMessagingScreen: React.FC<NewDirectMessagingScreenProps> =
338339
},
339340
]}
340341
>
341-
<Channel
342-
additionalTextInputProps={{
343-
onFocus: () => {
344-
setFocusOnMessageInput(true);
345-
setFocusOnSearchInput(false);
346-
if (messageInputRef.current) {
347-
messageInputRef.current.focus();
348-
}
349-
},
342+
<WithComponents
343+
overrides={{
344+
EmptyStateIndicator: EmptyMessagesIndicator,
345+
SendButton: NewDirectMessagingSendButton,
350346
}}
351-
audioRecordingEnabled={true}
352-
channel={currentChannel.current}
353-
EmptyStateIndicator={EmptyMessagesIndicator}
354-
enforceUniqueReaction
355-
keyboardVerticalOffset={0}
356-
onChangeText={setMessageInputText}
357-
overrideOwnCapabilities={{ sendMessage: true }}
358-
SendButton={NewDirectMessagingSendButton}
359-
setInputRef={(ref) => (messageInputRef.current = ref)}
360347
>
361-
{renderUserSearch({ inSafeArea: true })}
362-
{results && results.length >= 0 && !focusOnSearchInput && focusOnMessageInput && (
363-
<MessageList />
364-
)}
365-
<MessageComposer />
366-
</Channel>
348+
<Channel
349+
additionalTextInputProps={{
350+
onFocus: () => {
351+
setFocusOnMessageInput(true);
352+
setFocusOnSearchInput(false);
353+
if (messageInputRef.current) {
354+
messageInputRef.current.focus();
355+
}
356+
},
357+
}}
358+
audioRecordingEnabled={true}
359+
channel={currentChannel.current}
360+
enforceUniqueReaction
361+
keyboardVerticalOffset={0}
362+
onChangeText={setMessageInputText}
363+
overrideOwnCapabilities={{ sendMessage: true }}
364+
setInputRef={(ref) => (messageInputRef.current = ref)}
365+
>
366+
{renderUserSearch({ inSafeArea: true })}
367+
{results && results.length >= 0 && !focusOnSearchInput && focusOnMessageInput && (
368+
<MessageList />
369+
)}
370+
<MessageComposer />
371+
</Channel>
372+
</WithComponents>
367373
</SafeAreaView>
368374
);
369375
};

examples/SampleApp/src/screens/SharedGroupsScreen.tsx

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
33
import { NavigationProp, RouteProp, useNavigation } from '@react-navigation/native';
44
import {
55
ChannelList,
6-
ChannelListView,
7-
ChannelListViewProps,
86
ChannelPreviewViewProps,
97
getChannelPreviewDisplayAvatar,
108
GroupAvatar,
@@ -13,6 +11,7 @@ import {
1311
useTheme,
1412
Avatar,
1513
getInitialsFromName,
14+
WithComponents,
1615
} from 'stream-chat-react-native';
1716

1817
import { ScreenHeader } from '../components/ScreenHeader';
@@ -145,18 +144,19 @@ const EmptyListComponent = () => {
145144
);
146145
};
147146

148-
type ListComponentProps = ChannelListViewProps;
149-
150-
// If the length of channels is 1, which means we only got 1:1-distinct channel,
151-
// And we don't want to show 1:1-distinct channel in this list.
152-
const ListComponent: React.FC<ListComponentProps> = (props) => {
147+
// Custom empty state that also shows when there's only the 1:1 direct channel
148+
const SharedGroupsEmptyState = () => {
153149
const { channels, loadingChannels, refreshing } = useChannelsContext();
154150

155-
if (channels && channels.length <= 1 && !loadingChannels && !refreshing) {
151+
if (loadingChannels || refreshing) {
152+
return null;
153+
}
154+
155+
if (!channels || channels.length <= 1) {
156156
return <EmptyListComponent />;
157157
}
158158

159-
return <ChannelListView {...props} />;
159+
return null;
160160
};
161161

162162
type SharedGroupsScreenRouteProp = RouteProp<StackNavigatorParamList, 'SharedGroupsScreen'>;
@@ -179,19 +179,24 @@ export const SharedGroupsScreen: React.FC<SharedGroupsScreenProps> = ({
179179
return (
180180
<View style={styles.container}>
181181
<ScreenHeader titleText='Shared Groups' />
182-
<ChannelList
183-
filters={{
184-
$and: [{ members: { $in: [chatClient?.user?.id] } }, { members: { $in: [user.id] } }],
182+
<WithComponents
183+
overrides={{
184+
EmptyStateIndicator: SharedGroupsEmptyState,
185+
Preview: CustomPreview,
185186
}}
186-
List={ListComponent}
187-
options={{
188-
watch: false,
189-
}}
190-
Preview={CustomPreview}
191-
sort={{
192-
last_updated: -1,
193-
}}
194-
/>
187+
>
188+
<ChannelList
189+
filters={{
190+
$and: [{ members: { $in: [chatClient?.user?.id] } }, { members: { $in: [user.id] } }],
191+
}}
192+
options={{
193+
watch: false,
194+
}}
195+
sort={{
196+
last_updated: -1,
197+
}}
198+
/>
199+
</WithComponents>
195200
</View>
196201
);
197202
};

examples/SampleApp/src/screens/ThreadScreen.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
useTranslationContext,
1313
useTypingString,
1414
PortalWhileClosingView,
15+
WithComponents,
1516
} from 'stream-chat-react-native';
1617
import { useStateStore } from 'stream-chat-react-native';
1718

@@ -148,15 +149,19 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
148149

149150
return (
150151
<View style={[styles.container, { backgroundColor: white }]}>
152+
<WithComponents
153+
overrides={{
154+
// AttachmentPickerSelectionBar: CustomAttachmentPickerSelectionBar,
155+
MessageLocation,
156+
}}
157+
>
151158
<Channel
152159
audioRecordingEnabled={true}
153-
// AttachmentPickerSelectionBar={CustomAttachmentPickerSelectionBar}
154160
channel={channel}
155161
enforceUniqueReaction
156162
keyboardVerticalOffset={0}
157163
messageActions={messageActions}
158164
messageInputFloating={messageInputFloating}
159-
MessageLocation={MessageLocation}
160165
onPressMessage={onPressMessage}
161166
thread={thread}
162167
threadList
@@ -174,6 +179,7 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
174179
shouldUseFlashList={messageListImplementation === 'flashlist'}
175180
/>
176181
</Channel>
182+
</WithComponents>
177183
</View>
178184
);
179185
};

package/foobar.db-journal

-16.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)