# Overview

The core of Stream Chat React Native is its flexibility. Many apps only need [the theme](/chat/docs/sdk/react-native/customization/theming/). For deeper changes or new behavior, build a custom component.

## Best Practices

- Start with theming; only create custom components when behavior/layout must change.
- Use `WithComponents` to override default UI components at any level of the tree.
- Return `null` to remove UI features cleanly.
- Use the right context for your component's scope; avoid message-level re-renders.
- Keep custom components lightweight, especially inside `MessageList`.

## When to use a custom component

Build a custom component when you need to insert custom UI, significantly change layout, or modify behavior. With few exceptions, what you see is what you get.

### Exceptions

- Padding, fonts, colors, borders, etc. can all be altered from [the theme](/chat/docs/sdk/react-native/customization/theming/) as styles are passed to most components and sub-components.
- Messages can be aligned to one side using the `forceAlignMessages` prop on the `Channel` component.
- Message content can be reordered using the `messageContentOrder` prop on `Channel`; but you may want to adjust the theming to account for changes to border alignments if you alter this order.

Changes beyond these tweaks require custom components. To remove a UI feature, replace it with a component that returns `null`.

## How to use a custom component

Custom components are provided via the `WithComponents` wrapper. It accepts an `overrides` prop — an object mapping component names to your replacements. Wrap `WithComponents` around the part of the tree where the overrides should apply (typically around `Channel`, `ChannelList`, or even at the top level around `Chat`).

Overrides are inherited: inner `WithComponents` wrappers merge over outer ones, so you can set app-wide defaults and override specific components deeper in the tree.

To remove the fixed `DateHeader`, pass a component that returns `null`:

```tsx
import {
  WithComponents,
  Channel,
  MessageList,
  MessageInput,
} from "stream-chat-react-native";

<WithComponents overrides={{ DateHeader: () => null }}>
  <Channel channel={channel}>
    <MessageList />
    <MessageInput />
  </Channel>
</WithComponents>;
```

Your custom component replaces the default everywhere it is used within that subtree. Most components read from context; some also receive props. `DateHeader` receives a `dateString` prop.

```tsx
import {
  WithComponents,
  Channel,
  MessageList,
  MessageInput,
} from "stream-chat-react-native";

const MyNewComponent = ({ dateString }) => (
  <Text
    style={{ alignSelf: "center", backgroundColor: "red" }}
  >{`Hello World: ${dateString}`}</Text>
);

<WithComponents overrides={{ DateHeader: MyNewComponent }}>
  <Channel channel={channel}>
    <MessageList />
    <MessageInput />
  </Channel>
</WithComponents>;
```

![DateHeader](@chat-sdk/react-native/v9-latest/_assets/customization/custom-components/date_header.png)

![NoDateHeader](@chat-sdk/react-native/v9-latest/_assets/customization/custom-components/no_date_header.png)

![NewDateHeader](@chat-sdk/react-native/v9-latest/_assets/customization/custom-components/new_date_header.png)

## Using contexts

Custom components typically read from SDK contexts. Use [the hooks](/chat/docs/sdk/react-native/customization/contexts#hooks/) within their provider scope. Some contexts, like `MessageContext`, are only available inside a `Message`, so you can't use them in `DateHeader`.

You can also access the resolved set of components via the `useComponentsContext()` hook. This returns all components with defaults filled in, merged with any overrides from `WithComponents`.

Example: replace `DateHeader` with a component that reads `PaginatedMessageListContext` and displays the last sender.

```tsx
import {
  WithComponents,
  Channel,
  MessageList,
  MessageInput,
  usePaginatedMessageListContext,
} from "stream-chat-react-native";

const MySenderComponent = () => {
  const { messages } = usePaginatedMessageListContext();
  const latestMessageSender = messages[messages.length - 1]?.user?.name;

  return (
    <Text
      style={{ alignSelf: "center", backgroundColor: "red" }}
    >{`Last Sender: ${latestMessageSender}`}</Text>
  );
};

<WithComponents overrides={{ DateHeader: MySenderComponent }}>
  <Channel channel={channel}>
    <MessageList />
    <MessageInput />
  </Channel>
</WithComponents>;
```

![NameDateHeader](@chat-sdk/react-native/v9-latest/_assets/customization/custom-components/name_date_header.png)

This lets you tailor both UI and the information you show.

### Custom upload progress indicator

Pending attachment uploads are tracked by local attachment id through `client.uploadManager`. If you replace `AttachmentUploadIndicator`, use `usePendingAttachmentUpload(localId)` to read the same state as the default UI.

```tsx
import { Text, View } from "react-native";
import {
  Channel,
  MessageComposer,
  MessageList,
  WithComponents,
  usePendingAttachmentUpload,
} from "stream-chat-react-native";

const CustomAttachmentUploadIndicator = ({ localId }) => {
  const { isUploading, uploadProgress } = usePendingAttachmentUpload(localId);

  if (!isUploading) {
    return null;
  }

  return (
    <View>
      <Text>{Math.round(uploadProgress ?? 0)}%</Text>
    </View>
  );
};

<WithComponents
  overrides={{ AttachmentUploadIndicator: CustomAttachmentUploadIndicator }}
>
  <Channel channel={channel}>
    <MessageList />
    <MessageComposer />
  </Channel>
</WithComponents>;
```

<admonition type="warning">

Custom components within a `Message` should draw from mostly static contexts, except their own `MessageContext`. `ThemeContext`, `MessagesContext`, and `TranslationContext` are safe in most cases.

Memoizing values from contexts above the [`FlatList`](https://reactnative.dev/docs/flatlist) inside a row does not work well when those contexts update. Reading `PaginatedMessageListContext` inside each `Message` will cause the whole list to re-render.

</admonition>

## Common customization patterns

Here are some frequently requested customization examples:

### Custom message bubble

Replace the default message appearance with a custom design:

```tsx
import { View, Text, StyleSheet } from "react-native";
import {
  WithComponents,
  Channel,
  MessageList,
  MessageInput,
  useMessageContext,
} from "stream-chat-react-native";

const CustomMessageContent = () => {
  const { message, isMyMessage } = useMessageContext();

  return (
    <View
      style={[
        styles.messageContainer,
        isMyMessage ? styles.myMessage : styles.theirMessage,
      ]}
    >
      {!isMyMessage && (
        <Text style={styles.username}>{message.user?.name}</Text>
      )}
      <Text style={styles.messageText}>{message.text}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  messageContainer: {
    padding: 12,
    borderRadius: 16,
    maxWidth: "80%",
    marginVertical: 4,
  },
  myMessage: {
    backgroundColor: "#007AFF",
    alignSelf: "flex-end",
  },
  theirMessage: {
    backgroundColor: "#E5E5EA",
    alignSelf: "flex-start",
  },
  username: {
    fontSize: 12,
    color: "#666",
    marginBottom: 4,
  },
  messageText: {
    fontSize: 16,
  },
});

// Usage
<WithComponents overrides={{ MessageContent: CustomMessageContent }}>
  <Channel channel={channel}>
    <MessageList />
    <MessageInput />
  </Channel>
</WithComponents>;
```

### Custom send button

Create a send button with custom styling or behavior:

```tsx
import { TouchableOpacity, StyleSheet } from "react-native";
import {
  WithComponents,
  Channel,
  MessageList,
  MessageInput,
  useMessageInputContext,
  SendIcon,
} from "stream-chat-react-native";

const CustomSendButton = () => {
  const { sendMessage } = useMessageInputContext();

  return (
    <TouchableOpacity style={styles.sendButton} onPress={sendMessage}>
      <SendIcon />
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  sendButton: {
    backgroundColor: "#007AFF",
    borderRadius: 20,
    padding: 8,
    marginLeft: 8,
  },
});

// Usage
<WithComponents overrides={{ SendButton: CustomSendButton }}>
  <Channel channel={channel}>
    <MessageList />
    <MessageInput />
  </Channel>
</WithComponents>;
```

### Remove a UI element

To completely remove a UI element, pass a component that returns `null` in the overrides:

```tsx
import { WithComponents, Channel, MessageList, MessageInput } from "stream-chat-react-native";

// Remove the typing indicator and date header
<WithComponents overrides={{ TypingIndicator: () => null, DateHeader: () => null }}>
  <Channel channel={channel}>
    <MessageList />
    <MessageInput />
  </Channel>
</WithComponents>

// Remove message avatars
<WithComponents overrides={{ MessageAuthor: () => null }}>
  <Channel channel={channel}>
    <MessageList />
    <MessageInput />
  </Channel>
</WithComponents>
```


---

This page was last updated at 2026-05-22T16:32:09.900Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/customization/custom-components/](https://getstream.io/chat/docs/sdk/react-native/customization/custom-components/).