Skip to content

Commit 96b3cd3

Browse files
authored
fix: clean up ComponentOverrides types to not leak displayName (#3551)
## 🎯 Goal Components with `.displayName` assignments (e.g. `ChannelPreviewView.displayName = '...'`) caused `typeof` to include `displayName: string` (required) in the inferred `DEFAULT_COMPONENTS` type. This leaked into `ComponentOverrides`, forcing integrators to set `displayName` on their custom component overrides. ## 🛠 Implementation details - **`NormalizeComponents` mapped type** — normalizes each entry in `DEFAULT_COMPONENTS` to `React.ComponentType<P>`, where `displayName` is optional (`displayName?: string`). Applied via a typed cast on the export so the raw object keeps its runtime shape. - **`OptionalComponentOverrides` interface** — optional component slots (no default implementation) are moved out of the runtime object into a standalone interface. This eliminates `undefined as React.ComponentType<any> | undefined` casts and `eslint-disable` comments. - **Proper props types** — `MessageLocation`, `MessageText`, and `Input` now have real prop types instead of `React.ComponentType<any>`. Components rendered with no props use `React.ComponentType` (defaults to `{}`). - **Removed stale `PLAN.md`** from the componentsContext directory. ## 🎨 UI Changes No UI changes — types only. ## 🧪 Testing - `npx tsc --noEmit` passes with zero errors - Verified via TS compiler API that `ChannelPreview` on `DEFAULT_COMPONENTS` resolves to `ComponentType<...>` with `?displayName: string | undefined` (optional) ## ☑️ 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 - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
1 parent a9c64d9 commit 96b3cd3

File tree

4 files changed

+49
-229
lines changed

4 files changed

+49
-229
lines changed

package/src/contexts/componentsContext/ComponentsContext.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import React, { PropsWithChildren, useContext, useMemo } from 'react';
99
*/
1010
export type ComponentOverrides = Partial<
1111
(typeof import('./defaultComponents'))['DEFAULT_COMPONENTS']
12-
>;
12+
> &
13+
import('./defaultComponents').OptionalComponentOverrides;
1314

1415
const ComponentsContext = React.createContext<ComponentOverrides>({});
1516

package/src/contexts/componentsContext/PLAN.md

Lines changed: 0 additions & 148 deletions
This file was deleted.

package/src/contexts/componentsContext/__tests__/defaultComponents.test.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

package/src/contexts/componentsContext/defaultComponents.ts

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React from 'react';
2-
import { Image } from 'react-native';
2+
import { Image, ImageProps, TextInputProps } from 'react-native';
3+
4+
import type { LocalMessage, UserResponse } from 'stream-chat';
35

46
import { Attachment } from '../../components/Attachment/Attachment';
57
import { AudioAttachment } from '../../components/Attachment/Audio';
@@ -64,6 +66,7 @@ import { MessageReplies } from '../../components/Message/MessageItemView/Message
6466
import { MessageRepliesAvatars } from '../../components/Message/MessageItemView/MessageRepliesAvatars';
6567
import { MessageStatus } from '../../components/Message/MessageItemView/MessageStatus';
6668
import { MessageSwipeContent } from '../../components/Message/MessageItemView/MessageSwipeContent';
69+
import type { MessageTextProps } from '../../components/Message/MessageItemView/MessageTextContainer';
6770
import { MessageTimestamp } from '../../components/Message/MessageItemView/MessageTimestamp';
6871
import { ReactionListBottom } from '../../components/Message/MessageItemView/ReactionList/ReactionListBottom';
6972
import { ReactionListClustered } from '../../components/Message/MessageItemView/ReactionList/ReactionListClustered';
@@ -147,10 +150,16 @@ import { DefaultMessageOverlayBackground } from '../../contexts/overlayContext/M
147150
import type { MessageActionsProps } from '../../contexts/overlayContext/MessageOverlayHostLayer';
148151

149152
/**
150-
* All default component implementations used across the SDK.
151-
* These are the components used when no overrides are provided via WithComponents.
153+
* Normalizes each component entry to React.ComponentType<P>, stripping
154+
* extra inferred properties (like `displayName: string` from runtime
155+
* assignments) that would otherwise leak into the override types and
156+
* force integrators to match them.
152157
*/
153-
export const DEFAULT_COMPONENTS = {
158+
type NormalizeComponents<T> = {
159+
[K in keyof T]: T[K] extends React.ComponentType<infer P> ? React.ComponentType<P> : T[K];
160+
};
161+
162+
const components = {
154163
Attachment,
155164
AttachButton,
156165
AttachmentPickerContent,
@@ -300,35 +309,38 @@ export const DEFAULT_COMPONENTS = {
300309
MessageOverlayBackground: DefaultMessageOverlayBackground,
301310

302311
// Image
303-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
304-
ImageComponent: Image as React.ComponentType<any>,
305-
306-
// Optional overrides (no defaults — undefined unless user provides via WithComponents)
307-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
308-
AttachmentPickerIOSSelectMorePhotos: undefined as React.ComponentType<any> | undefined,
309-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
310-
ChatLoadingIndicator: undefined as React.ComponentType<any> | null | undefined,
311-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
312-
CreatePollContent: undefined as React.ComponentType<any> | undefined,
313-
MessageActions: undefined as React.ComponentType<MessageActionsProps> | undefined,
314-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
315-
Input: undefined as React.ComponentType<any> | undefined,
316-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
317-
ListHeaderComponent: undefined as React.ComponentType<any> | undefined,
318-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
319-
MessageContentBottomView: undefined as React.ComponentType<any> | undefined,
320-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
321-
MessageContentLeadingView: undefined as React.ComponentType<any> | undefined,
322-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
323-
MessageContentTopView: undefined as React.ComponentType<any> | undefined,
324-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
325-
MessageContentTrailingView: undefined as React.ComponentType<any> | undefined,
326-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
327-
MessageLocation: undefined as React.ComponentType<any> | undefined,
328-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
329-
MessageSpacer: undefined as React.ComponentType<any> | undefined,
330-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
331-
MessageText: undefined as React.ComponentType<any> | undefined,
332-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
333-
PollContent: undefined as React.ComponentType<any> | undefined,
312+
ImageComponent: Image as React.ComponentType<ImageProps>,
334313
};
314+
315+
/**
316+
* Optional component slots that have no default implementation.
317+
* These are `undefined` unless the integrator provides them via WithComponents.
318+
*/
319+
export interface OptionalComponentOverrides {
320+
AttachmentPickerIOSSelectMorePhotos?: React.ComponentType;
321+
ChatLoadingIndicator?: React.ComponentType | null;
322+
CreatePollContent?: React.ComponentType;
323+
Input?: React.ComponentType<{
324+
additionalTextInputProps?: TextInputProps;
325+
getUsers: () => UserResponse[];
326+
}>;
327+
ListHeaderComponent?: React.ComponentType;
328+
MessageActions?: React.ComponentType<MessageActionsProps>;
329+
MessageContentBottomView?: React.ComponentType;
330+
MessageContentLeadingView?: React.ComponentType;
331+
MessageContentTopView?: React.ComponentType;
332+
MessageContentTrailingView?: React.ComponentType;
333+
MessageLocation?: React.ComponentType<{ message: LocalMessage }>;
334+
MessageSpacer?: React.ComponentType;
335+
MessageText?: React.ComponentType<MessageTextProps>;
336+
PollContent?: React.ComponentType;
337+
}
338+
339+
/**
340+
* All default component implementations used across the SDK.
341+
* These are the components used when no overrides are provided via WithComponents.
342+
*
343+
* The `NormalizeComponents` cast ensures that internal details like
344+
* `displayName: string` don't leak into the public override types.
345+
*/
346+
export const DEFAULT_COMPONENTS: NormalizeComponents<typeof components> = components;

0 commit comments

Comments
 (0)