Skip to main content
This guide explains the mental model behind advanced UI Kit customization. The deep detail (file paths, symbol names) is grounded in the React v6 UI Kit; the same concept applies to every platform, with the per-platform mechanism summarized in the cross-platform table below.
The UI Kit’s extensibility is the classic Decorator pattern (GoF). Every “customization request” — adding an attachment option, a message template, a message action, a text formatter — is a method on a DataSource, and the kit composes a chain of DataSource decorators (one per enabled extension) on top of a base. Understanding this chain explains both why you append, not replace and why behavior is consistent across every enabled extension.

The four pieces (src/utils/)

How the chain is built

At CometChatUIKit.init(), each enabled extension’s .enable() is invoked, and each extension’s enable() calls:
So enabling Polls + Collaborative Document + Collaborative Whiteboard yields a chain whose head is the last-registered decorator:
CometChatUIKit.getDataSource() returns ChatConfigurator.getDataSource() — the head of that chain.

The call trace: “add a Share Location attachment option”

  1. The call hits the head decorator. It calls super.getAttachmentOptions(...), which walks down the chain to MessagesDataSource, returning the base [image, video, audio, file].
  2. On the way back up, each decorator runs its own override — const opts = super.getAttachmentOptions(...); opts.push(myExtensionAction); return opts; — so Polls pushes “Polls”, Doc pushes “Collaborative Document”, Whiteboard pushes “Collaborative Whiteboard”.
  3. The caller receives the fully-accumulated default list, appends its own CometChatMessageComposerAction, and hands the merged array to the composer.
The same shape powers getAllMessageTemplates() (each decorator pushes its custom-message template), getMessageOptions() (each pushes its long-press action), getAllTextFormatters(), etc.

Why this means “append, not replace”

Internally every decorator does super.getX().push(...). When you replace (pass a list containing only your item), you discard the entire chain’s accumulated output — which is why the default text/image/file bubbles, the camera/gallery/document attachments, or reply/edit/delete options silently vanish. The rule “start from getAllX() / the defaults, then push” literally mirrors the decorator chain. Pass only your item to a templates= / attachmentOptions= prop that replaces, and you have amputated the chain.
getAttachmentOptions and its second argument. getAttachmentOptions(id, additionalConfigurations?) threads the second argument through every decorator in the chain. A few extension decorators (Polls, Collaborative Document, Collaborative Whiteboard) read a field on it, so when you call it yourself, always pass a defined object — even if empty:
Because the chain runs every enabled decorator, omitting the argument can surface an error the instant any one of those extensions is active. Passing the defined object above is safe across kit versions.

Cross-platform: same concept, different mechanism

How the components are stitched together

The DataSource chain answers “what options/templates are available.” It is one of four stitch layers that together turn separate components into a working chat surface.

Layer 1 — Layout stitch (your app code)

The kit ships discrete components; you compose them. The canonical two-pane shape:
  • The Selector’s onItemClick stores selectedItem and passes it down as user OR group (mutually exclusive) to all three message components.
  • That shared user/group binding is the whole “which conversation” wiring — Header, List, and Composer independently fetch/scope to the same target.
  • There is no composite that does this for you in the current web/RN/Angular/Android kits — composing these four is the integration.

Layer 2 — Component → DataSource stitch (defaults vs override)

Each component, when you don’t pass the prop, pulls its config from the DataSource chain itself:
  • CometChatMessageListChatConfigurator.getDataSource().getAllMessageTemplates({...}).
  • CometChatMessageComposergetAttachmentOptions(...).
Passing templates= / attachmentOptions= replaces that internal fetch with your array — which is exactly why you must merge (start from getAllX() then push). This is the bridge between Layer 1 (props) and the decorator chain.

Layer 3 — Render stitch (message → bubble via the template map)

CometChatMessageList turns each message into a bubble through a category_type lookup map:
  1. It builds the map: messagesTypesArray[el.category + "_" + el.type] = el from the templates (yours-merged-with-defaults).
  2. For each message it resolves the slot views by key: messagesTypesMap[item.getCategory() + "_" + item.getType()]?.contentView(item, …) — and similarly headerView / footerView / bottomView / statusInfoView, or bubbleView to replace the whole bubble.
  3. getBubbleWrapper assembles those slot views into the rendered bubble.
So a custom message type renders only if there is a template whose type + category match the message — that’s the entire contract. No matching map entry → nothing (or an unknown-message fallback) renders. This is why the custom-message recipe is “register a template with type: "location", category: "custom" + a contentView.”

Layer 4 — Runtime stitch (the event bus decouples the components)

The components do not call each other directly. They communicate through pub/sub event buses — CometChatMessageEvents, CometChatUIEvents, CometChatGroupEvents:
  • CometChatUIKit.sendCustomMessage(msg) emits CometChatMessageEvents.ccMessageSent; the List is subscribed and appends the message optimistically → the bubble appears instantly (set sender on the message before sending so the optimistic bubble has one).
  • Incoming real-time messages arrive via the SDK message listener the List registers → appended the same way.
  • Group mutations (ccOwnershipChanged, ccGroupMemberAdded, …) flow the same pub/sub route, so the Header / Members views update without the List knowing about them.

The event-bus catalog (src/events/)

Six buses, two naming conventions. cc* = UI-Kit-emitted (a kit component reporting a local user action — subscribe to react to what the user did, often optimistically). on* = SDK pass-through (real-time inbound, mirroring the Chat SDK listeners — subscribe instead of registering a raw SDK listener). How to use them: subscribe with Bus.event.subscribe(cb) and always unsubscribe() on unmount (every kit component does). For your own code reacting to chat state, prefer these over raw SDK listeners — they’re already the kit’s source of truth and fire for both UI-originated and SDK-originated changes. Emit cc* yourself (e.g. ccMessageSent) only when you send a message outside CometChatUIKit.sendX and want the kit’s list to update — but CometChatUIKit.sendCustomMessage already emits it, which is why a custom bubble appears without any manual event work. Putting it together — the full flow for “send a custom location message”: Composer attachment onClicksendLocationMessage builds a CustomMessageCometChatUIKit.sendCustomMessage → emits ccMessageSentMessageList (subscribed) appends it → the render stitch looks up custom_location in the template map → calls your template’s contentView → your location bubble renders. On reload, the same bubble comes from history only if the messagesRequestBuilder includes the custom category + location type (Layer 2).

The mental model in one line

You stitch the layout (Layer 1); the kit stitches behavior via the DataSource chain for what’s available (Layer 2), the category_type template map for how each message renders (Layer 3), and the event bus for when things change (Layer 4). Customization = inserting your node into whichever layer owns the thing you want to change.

Source references

  • cometchat-uikit-react: src/utils/{ChatConfigurator,DataSource,DataSourceDecorator,MessagesDataSource}, src/components/Extensions/*/*ExtensionDecorator, src/CometChatUIKit/CometChatUIKit.ts.
  • The official React sample app’s live-location feature (attachment option + custom message + custom bubble) is a complete working reference for all four layers.