|
1 | | -# Plan: `WithComponents` Context Provider |
2 | | - |
3 | | -## Context |
4 | | - |
5 | | -The SDK prop-drills 120+ component overrides through `<Channel>` β `useCreate*Context` hooks β context values. Each component name is listed **4 times** (destructured from props β passed to hook β destructured in hook β listed in useMemo). Consumers then read components back out via `useMessagesContext()`, `useMessageInputContext()`, etc. |
6 | | - |
7 | | -**Goal**: Replace this entire pipeline with a single `ComponentsContext`. Component overrides are **removed** from all existing contexts. Consumers read components via `useComponentsContext()` instead. |
8 | | - |
9 | | -```tsx |
10 | | -// User API |
11 | | -<WithComponents value={{ Message: MyMessage, SendButton: MySendButton }}> |
12 | | - <Channel channel={channel}> |
13 | | - <MessageList /> |
14 | | - <MessageInput /> |
15 | | - </Channel> |
16 | | -</WithComponents> |
17 | | -``` |
| 1 | +# WithComponents β Component Override System |
18 | 2 |
|
19 | 3 | ## Design Principle |
20 | 4 |
|
21 | 5 | **All components are read from `useComponentsContext()`. All other contexts only provide data + APIs β never components.** |
22 | 6 |
|
23 | | -No context besides `ComponentsContext` should carry component references. This is the single rule that drives every change in this plan. |
| 7 | +## Current State (Completed) |
24 | 8 |
|
25 | | -## Architecture |
| 9 | +### What was done |
26 | 10 |
|
27 | | -### Before |
| 11 | +1. **Created `ComponentsContext`** β `WithComponents` provider, `useComponentsContext()` hook, `ComponentOverrides` type |
| 12 | +2. **Created `defaultComponents.ts`** β centralized map of all ~130 default components |
| 13 | +3. **Stripped component keys** from all existing context types: `MessagesContextValue`, `InputMessageInputContextValue`, `ChannelContextValue`, `ChannelsContextValue`, `AttachmentPickerContextValue`, `ThreadsContextValue`, `ImageGalleryContextValue` |
| 14 | +4. **Simplified `useCreate*Context` hooks** β no longer receive or forward component params |
| 15 | +5. **Simplified `Channel.tsx`** β removed ~90 component imports, prop defaults, forwarding lines |
| 16 | +6. **Simplified `ChannelList.tsx`** β removed ~19 component props |
| 17 | +7. **Updated ~80 consumer files** β switched from old context hooks to `useComponentsContext()` |
| 18 | +8. **Removed component override props** from ALL individual components |
| 19 | +9. **Updated all 3 example apps** (SampleApp, ExpoMessaging, TypeScriptMessaging) |
| 20 | +10. **Updated ~45 documentation pages** across docs-content repo |
| 21 | +11. **Merged with develop** and resolved conflicts |
28 | 22 |
|
29 | | -``` |
30 | | -Channel props (90+ component overrides with defaults) |
31 | | - β useCreateMessagesContext (receives 60+ component params, maps into useMemo) |
32 | | - β MessagesContext carries components + runtime data |
33 | | - β Consumer: const { Message } = useMessagesContext() |
34 | | -``` |
35 | | - |
36 | | -### After |
| 23 | +### Architecture |
37 | 24 |
|
38 | 25 | ``` |
39 | | -DEFAULT_COMPONENTS (static map) |
40 | | - β ComponentsContext (defaults; user overrides via WithComponents) |
41 | | - β Consumer: const { Message } = useComponentsContext() |
42 | | -
|
43 | | -Channel props (runtime/config only) |
44 | | - β useCreateMessagesContext (runtime data only, no components) |
45 | | - β MessagesContext carries ONLY data + APIs |
46 | | - β Consumer: const { deleteMessage } = useMessagesContext() |
| 26 | +User: <WithComponents overrides={{ Message: Custom }}> |
| 27 | + β |
| 28 | +ComponentsContext (merges parent + overrides, inner wins) |
| 29 | + β |
| 30 | +useComponentsContext() β { ...DEFAULT_COMPONENTS, ...overrides } |
| 31 | + β |
| 32 | +Consumer: const { Message } = useComponentsContext() |
47 | 33 | ``` |
48 | 34 |
|
49 | | -## Scope |
50 | | - |
51 | | -### What changes |
52 | | - |
53 | | -1. **Existing context types** (`MessagesContextValue`, `InputMessageInputContextValue`, `ChannelContextValue`, `ChannelsContextValue`, `AttachmentPickerContextValue`) β remove all component-type keys |
54 | | -2. **`useCreate*Context` hooks** β remove all component params, stop mapping them |
55 | | -3. **Channel.tsx** β remove ~90 component imports, ~90 destructuring defaults, ~90 forwarding lines |
56 | | -4. **ChannelList.tsx** β remove ~19 component props and forwarding |
57 | | -5. **~117 consumer callsites across ~97 files** β switch from `useXContext()` to `useComponentsContext()` for component reads |
58 | | - |
59 | | -### What doesn't change |
| 35 | +### Key Files |
60 | 36 |
|
61 | | -- Runtime data flow (callbacks like `deleteMessage`, `sendReaction`, state like `targetedMessage`) stays in existing contexts |
62 | | -- Consumer reads of runtime data (`const { deleteMessage } = useMessagesContext()`) are untouched |
63 | | -- `WithComponents` nesting semantics (inner wins, like standard React context) |
| 37 | +| File | Purpose | |
| 38 | +|------|---------| |
| 39 | +| `ComponentsContext.tsx` | ~60 lines. `ComponentOverrides` type (derived from `typeof DEFAULT_COMPONENTS`), `WithComponents` provider, `useComponentsContext()` hook | |
| 40 | +| `defaultComponents.ts` | ~300 lines. Single source of truth for all default component mappings. Adding a new component here auto-extends `ComponentOverrides` | |
64 | 41 |
|
65 | | -## New Files |
| 42 | +### Type System |
66 | 43 |
|
67 | | -| File | Purpose | |
68 | | -| -------------------------------------------------------------- | ----------------------------------------------------------------------------------- | |
69 | | -| `package/src/contexts/componentsContext/ComponentsContext.tsx` | `ComponentOverrides` type, `WithComponents` provider, `useComponentsContext()` hook | |
70 | | -| `package/src/contexts/componentsContext/defaultComponents.ts` | All default component imports β `DEFAULT_COMPONENTS` map | |
71 | | - |
72 | | -Both already drafted in the repo. |
73 | | - |
74 | | -## Implementation Steps |
75 | | - |
76 | | -### Step 1: Finalize `ComponentsContext.tsx` and `defaultComponents.ts` |
77 | | - |
78 | | -Already drafted. Key design: |
79 | | - |
80 | | -- `ComponentOverrides`: flat map, all keys optional, explicitly typed per component |
81 | | -- Context default = `DEFAULT_COMPONENTS` β `useComponentsContext()` always returns resolved values |
82 | | -- `WithComponents`: merges `{ ...parent, ...value }` (inner wins) |
83 | | -- `ResolvedComponents` = `Required<ComponentOverrides>` for the return type |
84 | | - |
85 | | -Special cases: |
86 | | - |
87 | | -- `FlatList` β from `NativeHandlers.FlatList` at runtime. Keep as a runtime prop in MessagesContext, not in ComponentsContext. |
88 | | -- `StopMessageStreamingButton` β can be `null` (explicitly hide). The type in ComponentOverrides allows `| null`. |
| 44 | +`ComponentOverrides` is derived automatically: |
| 45 | +```ts |
| 46 | +export type ComponentOverrides = Partial< |
| 47 | + (typeof import('./defaultComponents'))['DEFAULT_COMPONENTS'] |
| 48 | +>; |
| 49 | +``` |
89 | 50 |
|
90 | | -### Step 2: Strip component keys from existing context value types |
| 51 | +No manual type maintenance β add a component to `DEFAULT_COMPONENTS` and the type updates. |
91 | 52 |
|
92 | | -**`MessagesContextValue`** (`package/src/contexts/messagesContext/MessagesContext.tsx`): |
93 | | -Remove ~60 component keys (Attachment, AudioAttachment, DateHeader, Message, MessageContent, Reply, etc.). Keep runtime keys only (deleteMessage, deleteReaction, dismissKeyboardOnMessageTouch, giphyVersion, messageContentOrder, etc.). |
| 53 | +### Circular Dependency Handling |
94 | 54 |
|
95 | | -**`InputMessageInputContextValue`** (`package/src/contexts/messageInputContext/MessageInputContext.tsx`): |
96 | | -Remove ~35 component keys (AttachButton, AudioRecorder, SendButton, Input, InputView, etc.). Keep runtime keys only (asyncMessagesLockDistance, audioRecordingEnabled, editMessage, sendMessage, etc.). |
| 55 | +`defaultComponents.ts` β imports components β components import `useComponentsContext` from `ComponentsContext.tsx`. |
97 | 56 |
|
98 | | -**`ChannelContextValue`** (`package/src/contexts/channelContext/ChannelContext.tsx`): |
99 | | -Remove 4 component keys (EmptyStateIndicator, LoadingIndicator, NetworkDownIndicator, StickyHeader). |
| 57 | +Broken by lazy-loading defaults in the hook: |
| 58 | +```ts |
| 59 | +let cachedDefaults: ComponentOverrides | undefined; |
| 60 | +const getDefaults = () => { |
| 61 | + if (!cachedDefaults) { |
| 62 | + cachedDefaults = require('./defaultComponents').DEFAULT_COMPONENTS; |
| 63 | + } |
| 64 | + return cachedDefaults; |
| 65 | +}; |
| 66 | +``` |
100 | 67 |
|
101 | | -**`ChannelsContextValue`** (`package/src/contexts/channelsContext/ChannelsContext.tsx`): |
102 | | -Remove ~19 component keys (Preview, PreviewAvatar, PreviewMessage, Skeleton, FooterLoadingIndicator, etc.). |
| 68 | +### Naming Conventions |
103 | 69 |
|
104 | | -**`AttachmentPickerContextValue`** (`package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx`): |
105 | | -Remove 3 component keys (ImageOverlaySelectedComponent, AttachmentPickerSelectionBar, AttachmentPickerContent). |
| 70 | +Some component keys differ from their default component names to avoid collisions: |
106 | 71 |
|
107 | | -### Step 3: Simplify `useCreate*Context` hooks |
| 72 | +| Override Key | Default Component | Why renamed | |
| 73 | +|---|---|---| |
| 74 | +| `FileAttachmentIcon` | `FileIcon` | Clarity | |
| 75 | +| `ChannelListLoadingIndicator` | `ChannelListLoadingIndicator` | Split from shared `LoadingIndicator` β renders skeleton UI | |
| 76 | +| `MessageListLoadingIndicator` | `LoadingIndicator` | Split from shared `LoadingIndicator` β renders text | |
| 77 | +| `ChatLoadingIndicator` | `undefined` | Optional, no default | |
| 78 | +| `ThreadMessageComposer` | `MessageComposer` | Avoid collision with `MessageComposer` component name | |
| 79 | +| `ThreadListComponent` | `DefaultThreadListComponent` | Avoid collision with exported `ThreadList` | |
| 80 | +| `StartAudioRecordingButton` | `AudioRecordingButton` | Historical naming | |
| 81 | +| `Preview` | `ChannelPreviewView` | ChannelList preview item | |
| 82 | +| `PreviewAvatar` | `ChannelAvatar` | ChannelList preview avatar | |
| 83 | +| `FooterLoadingIndicator` | `ChannelListFooterLoadingIndicator` | ChannelList footer | |
| 84 | +| `HeaderErrorIndicator` | `ChannelListHeaderErrorIndicator` | ChannelList header | |
| 85 | +| `HeaderNetworkDownIndicator` | `ChannelListHeaderNetworkDownIndicator` | ChannelList header | |
108 | 86 |
|
109 | | -Each hook drops all component params and stops mapping them into useMemo: |
| 87 | +### Optional Components (no default) |
110 | 88 |
|
111 | | -- **`useCreateMessagesContext`** (`package/src/components/Channel/hooks/useCreateMessagesContext.ts`): ~60 component params removed, keep ~30 runtime params |
112 | | -- **`useCreateInputMessageInputContext`** (`package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts`): ~35 component params removed, keep ~15 runtime params |
113 | | -- **`useCreateChannelContext`** (`package/src/components/Channel/hooks/useCreateChannelContext.ts`): 4 component params removed |
114 | | -- **`useCreateChannelsContext`** (`package/src/components/ChannelList/hooks/useCreateChannelsContext.ts`): ~19 component params removed, keep ~20 runtime params |
| 89 | +These exist in `DEFAULT_COMPONENTS` as `undefined` with `React.ComponentType<any> | undefined` type assertions: |
115 | 90 |
|
116 | | -### Step 4: Simplify Channel.tsx |
| 91 | +`AttachmentPickerIOSSelectMorePhotos`, `ChatLoadingIndicator`, `CreatePollContent`, `ImageComponent`, `Input`, `ListHeaderComponent`, `MessageContentBottomView`, `MessageContentLeadingView`, `MessageContentTopView`, `MessageContentTrailingView`, `MessageLocation`, `MessageSpacer`, `MessageText`, `PollContent` |
117 | 92 |
|
118 | | -- Remove ~90 default component imports (lines 114-223) |
119 | | -- Remove component keys from `ChannelPropsWithContext` type |
120 | | -- Remove component destructuring defaults from `ChannelWithContext` |
121 | | -- Remove component values from `useCreateMessagesContext()`, `useCreateInputMessageInputContext()`, `useCreateChannelContext()` calls |
122 | | -- Remove component values from `attachmentPickerContext` useMemo |
123 | | -- `LoadingErrorIndicator` and `KeyboardCompatibleView` are used directly in Channel's JSX β read from `useComponentsContext()` or keep as Channel-specific props |
| 93 | +### Shared Component Keys (audited) |
124 | 94 |
|
125 | | -### Step 5: Simplify ChannelList.tsx |
| 95 | +Some keys were used in multiple contexts before the refactor. Audit results: |
126 | 96 |
|
127 | | -- Remove component keys from `ChannelListProps` type |
128 | | -- Remove default component imports |
129 | | -- Remove component values from `useCreateChannelsContext()` call |
| 97 | +| Key | Used By | Same Default? | Resolution | |
| 98 | +|-----|---------|---------------|------------| |
| 99 | +| `EmptyStateIndicator` | Channel + ChannelList | Yes (differentiates via `listType` prop) | Single key β
| |
| 100 | +| `LoadingErrorIndicator` | Channel + ChannelList | Yes (differentiates via `listType` prop) | Single key β
| |
| 101 | +| `LoadingIndicator` | Channel + ChannelList | **No** β Channel used text-based, ChannelList used skeleton | Split into `MessageListLoadingIndicator` + `ChannelListLoadingIndicator` β
| |
130 | 102 |
|
131 | | -### Step 6: Update ~117 consumer callsites |
| 103 | +### API Alignment with stream-chat-react |
132 | 104 |
|
133 | | -Switch component destructuring from old context hooks to `useComponentsContext()`: |
| 105 | +| Aspect | React Native | React Web | |
| 106 | +|--------|-------------|-----------| |
| 107 | +| Provider | `WithComponents` | `WithComponents` | |
| 108 | +| Prop name | `overrides` | `overrides` | |
| 109 | +| Hook | `useComponentsContext()` | `useComponentContext()` | |
| 110 | +| Type | `ComponentOverrides` (auto-derived) | `ComponentContextValue` (hand-written) | |
| 111 | +| Defaults | Lazy-loaded via `require()` | Set at `Channel` level | |
| 112 | +| Merge | `useMemo` | Plain spread (no memo) | |
134 | 113 |
|
135 | | -```tsx |
136 | | -// Before |
137 | | -const { Message, MessageStatus, MessageTimestamp } = useMessagesContext(); |
138 | | -const { deleteMessage } = useMessagesContext(); |
| 114 | +## Known Issues / Future Work |
139 | 115 |
|
140 | | -// After |
141 | | -const { Message, MessageStatus, MessageTimestamp } = useComponentsContext(); |
142 | | -const { deleteMessage } = useMessagesContext(); |
143 | | -``` |
| 116 | +### Pre-existing Test Failures (not caused by this work) |
144 | 117 |
|
145 | | -**Key files by volume** (largest consumers): |
| 118 | +These test suites fail on `develop` too: |
| 119 | +- `offline-support/index.test.ts` β timeout |
| 120 | +- `ChannelList.test.js` β filter race condition (`channel.countUnread` mock missing) |
| 121 | +- `isAttachmentEqualHandler.test.js`, `MessageContent.test.js`, `MessageTextContainer.test.tsx`, `MessageUserReactions.test.tsx`, `ChannelPreview.test.tsx` β various pre-existing issues |
146 | 122 |
|
147 | | -- `components/Message/MessageItemView/MessageItemView.tsx` β 15+ component keys |
148 | | -- `components/Message/MessageItemView/MessageContent.tsx` β 15+ component keys |
149 | | -- `components/MessageList/MessageList.tsx` β 10+ component keys |
150 | | -- `components/MessageList/MessageFlashList.tsx` β 10+ component keys |
151 | | -- `components/MessageInput/MessageComposer.tsx` β 25+ component keys |
152 | | -- `components/Attachment/Attachment.tsx` β 10+ component keys |
153 | | -- `components/ChannelList/ChannelListView.tsx` β multiple component keys |
154 | | -- `components/ChannelPreview/ChannelPreviewView.tsx` β multiple component keys |
| 123 | +### Linter Interaction |
155 | 124 |
|
156 | | -Many other files destructure just 1-2 component keys from context β straightforward replacements. |
| 125 | +`@typescript-eslint/no-unused-vars` (warn, max-warnings 0) aggressively strips unused type keys. When adding new keys to `ComponentOverrides`, the type and its consumer must land in the same edit β otherwise the linter removes the key between saves. |
157 | 126 |
|
158 | | -### Step 7: Update exports |
| 127 | +Since `ComponentOverrides` is now auto-derived from `DEFAULT_COMPONENTS`, this is no longer an issue for the type itself. But be aware when adding optional components (`undefined as React.ComponentType<any> | undefined`). |
159 | 128 |
|
160 | | -- `package/src/contexts/index.ts` β add `export * from './componentsContext/ComponentsContext'` |
161 | | -- `package/src/index.ts` β verify `WithComponents`, `ComponentOverrides`, `useComponentsContext` are exported |
| 129 | +### `contexts/index.ts` Barrel Export |
162 | 130 |
|
163 | | -### Step 8: Update tests |
| 131 | +The `export * from './componentsContext/ComponentsContext'` line in `contexts/index.ts` was stripped by the linter multiple times during development. If `WithComponents` becomes unexportable from the package, check this barrel file first. |
164 | 132 |
|
165 | | -Tests that pass component overrides as Channel/ChannelList props will need to wrap in `<WithComponents>` instead. Mock builders that set up context values with component overrides may also need updating. |
| 133 | +### Documentation |
166 | 134 |
|
167 | | -## Edge Cases |
| 135 | +Docs PR: https://github.com/GetStream/docs-content/pull/1169 |
168 | 136 |
|
169 | | -- **Shared names**: `EmptyStateIndicator`, `LoadingIndicator` exist in both Channel and ChannelList. One key in flat map β users use nesting for different overrides per area. |
170 | | -- **Mixed destructuring**: Some consumers destructure both components and runtime data from the same `useMessagesContext()` call. These need to be split into two calls. |
171 | | -- **`FlatList`**: Runtime-resolved from NativeHandlers. Stays in MessagesContext as a runtime value, not in ComponentsContext. |
172 | | -- **`StopMessageStreamingButton`**: Supports `null` to hide. ComponentOverrides type allows `| null`. |
| 137 | +Updated ~45 pages across: |
| 138 | +- Core teaching pages (custom_components, message-customization, etc.) |
| 139 | +- Component reference pages (channel-list, message-list, message-composer, etc.) |
| 140 | +- Context docs (stripped component keys from 7 context pages) |
| 141 | +- Migration guide (upgrading-from-v8.md β comprehensive WithComponents section) |
| 142 | +- Advanced guides (audio, AI, image-picker, etc.) |
173 | 143 |
|
174 | | -## Verification |
| 144 | +### SDK PR |
175 | 145 |
|
176 | | -1. `cd package && yarn build` β type-checks and builds |
177 | | -2. `cd package && yarn test:unit` β tests pass (after updating test fixtures) |
178 | | -3. `cd package && yarn lint` β no lint errors |
179 | | -4. Manual: `<WithComponents value={{ Message: Custom }}>` β verify override appears |
180 | | -5. Verify nesting: inner `WithComponents` wins over outer |
| 146 | +https://github.com/GetStream/stream-chat-react-native/pull/3542 |
0 commit comments