# Custom Attachments

## How to build a custom attachment

Attachments are a property on the `message` object:

```js
const messageObject = {
  id: "12312jh3b1jh2b312",
  text: "This is my test message!",
  attachments: [
    {
      type: "image",
      thumb_url: "",
    },
    {
      type: "file",
      asset_url: "",
    },
  ],
};
```

Attachment rendering depends on `message.attachments[index].type`. Built-in views:

- attachment type `audio` - [AudioAttachment](/chat/docs/sdk/react-native/ui-components/audio-attachment/)
- attachment type `video` - renders a [VideoThumbnail](/chat/docs/sdk/react-native/ui-components/video-thumbnail/) in the Gallery.
- attachment type `image` - [Gallery](/chat/docs/sdk/react-native/ui-components/gallery/) (single or multiple images)
- attachment type `giphy` - [Giphy](/chat/docs/sdk/react-native/ui-components/giphy/)
- attachment type `file` - [FileAttachment](/chat/docs/sdk/react-native/ui-components/file-attachment/)
- attachment with URL - [URLPreview](/chat/docs/sdk/react-native/ui-components/url-preview/)
- compact URL preview - [URLPreviewCompact](/chat/docs/sdk/react-native/ui-components/url-preview-compact/)

## Best Practices

- Keep custom attachment `type` values unique and stable so renderers stay deterministic.
- Pass custom attachment renderers via `WithComponents` to avoid forking the message list.
- Include enough metadata on the attachment object to render without extra network calls.
- Fall back to the default `UnsupportedAttachment` renderer for unknown types to avoid blank messages.
- Validate attachment payloads before sending to prevent malformed renders for other clients.

<table>
  <tr>
  <td align='center' width='25%'>

![Gallery](@chat-sdk/react-native/v9-latest/_assets/ui-cookbook/custom-attachments/gallery.png)

  </td>
  <td align='center' width='25%'>

![Giphy](@chat-sdk/react-native/v9-latest/_assets/ui-cookbook/custom-attachments/giphy.png)

  </td>
  <td align='center' width='25%'>

![URLPreview](@chat-sdk/react-native/v9-latest/_assets/ui-cookbook/custom-attachments/url_preview.png)

  </td>
  <td align='center' width='25%'>

![URLPreviewCompact](@chat-sdk/react-native/v9-latest/_assets/ui-cookbook/custom-attachments/url_preview_compact.png)

  </td>
  </tr>
  <tr></tr>
  <tr>
  <td align='center'>
      <strong>Gallery</strong>
  </td>
  <td align='center'>
      <strong>Giphy</strong>
  </td>
  <td align='center'>
      <strong>UrlPreview</strong>
  </td>
  <td align='center'>
      <strong>URLPreviewCompact</strong>
  </td>
  </tr>
</table>

<table>
  <tr>
  <td align='center' width='33%'>

![FileAttachment](@chat-sdk/react-native/v9-latest/_assets/ui-cookbook/custom-attachments/files.png)

  </td>
  <td align='center' width='33%'>

![AudioAttachment](@chat-sdk/react-native/v9-latest/_assets/ui-cookbook/custom-attachments/audio.png)

  </td>
  <td align='center' width='33%'>

![VideoThumbnail](@chat-sdk/react-native/v9-latest/_assets/ui-cookbook/custom-attachments/video.png)

  </td>
  <td align='center' width='33%'>

![UnsupportedAttachment](@chat-sdk/react-native/v9-latest/_assets/ui-cookbook/custom-attachments/unsupported_attachment.png)

  </td>
  </tr>
  <tr></tr>
  <tr>
  <td align='center'>
      <strong>FileAttachment</strong>
  </td>
  <td align='center'>
      <strong>AudioAttachment</strong>
  </td>
  <td align='center'>
      <strong>VideoThumbnail</strong>
  </td>
  <td align='center'>
      <strong>UnsupportedAttachment</strong>
  </td>
  </tr>
</table>

Default components handle these types in MessageList. You can override them using `WithComponents`, for example:

```tsx
import { WithComponents } from 'stream-chat-react-native';

const CustomGiphy = ({ attachment, onPressIn }) => {
  console.log(attachment.type);
  console.log(attachment.actions)
  console.log(attachment.image_url)
  console.log(attachment.thumb_url)
  console.log(attachment.title)
  console.log(attachment.type)

  return (/** Your custom UI */)
}

const CustomGallery = ({ images, onPressIn }) => {
  console.log(images);

  return (/** Your custom UI */)
}

const CustomFileAttachment = ({ attachment }) => {
  console.log(attachment.mime_type);
  console.log(attachment.title);
  console.log(attachment.file_size);
  console.log(attachment.actions);

  return (/** Your custom UI */)
}

const CustomUrlPreview = () => {
  console.log(text);
  console.log(thumb_url);
  console.log(title);

  return (/** Your custom UI */)
}

// Provide these custom components via WithComponents.
<WithComponents overrides={{
  Gallery: CustomGallery,
  Giphy: CustomGiphy,
  FileAttachment: CustomFileAttachment,
  UrlPreview: CustomUrlPreview,
}}>
  <Channel channel={channel}>
    {/* The underlying components */}
  </Channel>
</WithComponents>
```

You can also use a custom `type`. Custom attachments render in the `Card` view by default, which you can override.

```tsx
import { WithComponents } from 'stream-chat-react-native';

const CustomCardComponent = ({ type, ...otherProperties }) => {
  console.log(type);
  console.log(otherProperties);

  return (/** Your custom UI */)
}

<WithComponents overrides={{ Card: CustomCardComponent }}>
  <Channel channel={channel}>
    {/* The underlying components */}
  </Channel>
</WithComponents>
```

## Handling Custom Properties On Attachment

Default UI components and context providers from the SDK are memoized for performance purpose, and will not trigger re-renders upon updates to
custom properties on attachment.

Eg: Suppose we add a `customField` property to the attachment object and use it the UI in custom `UnsupportedAttachment` component.

```tsx
import { WithComponents } from "stream-chat-react-native";

const CustomUnsupportedAttachment = (attachment) => {
  return (
    <View>
      <Text>{attachment.customField}</Text>
    </View>
  );
};

<WithComponents
  overrides={{ UnsupportedAttachment: CustomUnsupportedAttachment }}
>
  <Channel channel={channel}>{/* The underlying components */}</Channel>
</WithComponents>;
```

In this example, if you try to update `customField` on particular attachment from backend (or anywhere), you will not see it updated on UI until you refresh the chat.

The reason being, the default memoization logic only checks for fixed set of properties on attachment, and doesn't check for custom properties.

This can be solved by providing a function which checks for changes in custom properties which you may have been defined on attachment.

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

const isAttachmentEqualHandler = (prevAttachment, nextAttachment) => {
  const attachmentEqual =
    prevAttachment.customField === nextAttachment.customField;
  if (!attachmentEqual) return false;
  return true;
};

const CustomUnsupportedAttachment = (attachment) => {
  return (
    <View>
      <Text>{attachment.customField}</Text>
    </View>
  );
};

<WithComponents
  overrides={{ UnsupportedAttachment: CustomUnsupportedAttachment }}
>
  <Channel channel={channel} isAttachmentEqual={isAttachmentEqualHandler}>
    {/* The underlying components */}
  </Channel>
</WithComponents>;
```


---

This page was last updated at 2026-07-10T16:05:08.913Z.

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