The CometChat Angular UIKit uses a Hybrid Approach for managing active chat state. At its core, ChatStateService acts as a single source of truth for the currently active chat entity — a CometChat.User, CometChat.Group, or CometChat.Conversation.The Hybrid Approach gives you two ways to wire components:
Service-based (default) — Components automatically subscribe to ChatStateService and react when the active entity changes. No prop passing required.
Props override — Pass [user] or [group] via @Input() bindings to override the service state for a specific component instance.
This design exists because most applications need a simple, zero-config wiring mechanism (service-based), while advanced use cases — like multi-panel layouts or isolated chat windows — require explicit control (props-based). The Hybrid Approach delivers both without forcing a choice upfront.ChatStateService is provided at the root level (providedIn: 'root') and exposes both Angular Signals and RxJS Observables, so you can choose whichever reactive API fits your codebase.
Use this table to decide which pattern fits your use case:
Criteria
Service-Based
Props-Based
Setup complexity
Zero config — components auto-subscribe
Manual — you manage and pass state
Boilerplate
Minimal
More wiring code
Multi-panel layouts
Not ideal — single active entity
Required — each panel gets its own entity
Component isolation
Shared state across all instances
Each instance can have independent state
When to use
Single chat view, standard layouts
Multi-panel, embedded widgets, testing
State sync
Automatic across all Chat-Aware Components
You control sync manually
Recommended for
Most applications
Advanced or custom layouts
Start with the service-based pattern. It covers the majority of use cases with zero boilerplate.
Switch to props-based only when you need independent state per component instance.
With the service-based pattern, you place Chat-Aware Components in your template and they automatically subscribe to ChatStateService. When a user selects a conversation, the service updates and all downstream components react — no explicit binding needed.
<cometchat-conversations> calls ChatStateService.setActiveConversation() when a conversation is clicked
setActiveConversation() extracts the CometChat.User or CometChat.Group from the conversation and sets it as the active entity
<cometchat-message-header>, <cometchat-message-list>, and <cometchat-message-composer> automatically subscribe to the active user/group and update their UI
This is the recommended approach for most applications. It reduces boilerplate and keeps
components in sync automatically. You don’t need to handle (itemClick) events or pass
data between components — ChatStateService handles the wiring.
With the props-based pattern, you manage state yourself and pass [user] or [group] directly to each component via @Input() bindings. This gives you full control over which entity each component displays.
When [user] or [group]@Input() bindings are provided, they take priority over
ChatStateService state for that component instance. Other component instances without
explicit bindings continue to read from the service.
ChatStateService enforces that only one chat entity — a CometChat.User or a CometChat.Group — can be active at any given time:
Calling setActiveUser(user) with a non-null value automatically sets activeGroup to null
Calling setActiveGroup(group) with a non-null value automatically sets activeUser to null
Calling setActiveConversation(conversation) extracts the entity and delegates to setActiveUser() or setActiveGroup(), applying the same rule
Calling clearActiveChat() resets all state to null
This prevents ambiguous states where both a user and a group appear active simultaneously.
import { Component, inject } from '@angular/core';import { CometChat } from '@cometchat/chat-sdk-javascript';import { ChatStateService } from '@cometchat/chat-uikit-angular';@Component({ selector: 'app-chat-nav', standalone: true, template: ` <button (click)="selectUser()">Chat with Alice</button> <button (click)="selectGroup()">Open Team Chat</button> <p>Active user: {{ chatStateService.activeUser()?.getName() ?? 'none' }}</p> <p>Active group: {{ chatStateService.activeGroup()?.getName() ?? 'none' }}</p> `})export class ChatNavComponent { chatStateService = inject(ChatStateService); selectUser(): void { const user = new CometChat.User('alice'); user.setName('Alice'); this.chatStateService.setActiveUser(user); // At this point: // chatStateService.activeUser() → Alice // chatStateService.activeGroup() → null (automatically cleared) } selectGroup(): void { const group = new CometChat.Group('team', 'Team Chat', 'public'); this.chatStateService.setActiveGroup(group); // At this point: // chatStateService.activeGroup() → Team Chat // chatStateService.activeUser() → null (automatically cleared) }}
Setting a user clears the group, and setting a group clears the user. You do not need
to manually clear the previous entity before setting a new one — ChatStateService
handles this automatically.
For applications that display multiple chat panels side by side (e.g., a support dashboard), use the props-based pattern to give each panel its own independent state:
Each panel receives its own [user] or [group] binding, so they operate independently
of ChatStateService. This avoids the mutual exclusivity constraint that applies to
service-based state.
Scoping Customization Services for Multiple Instances
The sections above cover ChatStateService and how to use props to give each panel its own data. But there’s a related concern: customization services like MessageBubbleConfigService and FormatterConfigService are also root-level singletons. If you configure custom bubble templates or formatters globally, those customizations apply to every message list in the app.When you need different customizations per panel (e.g., a main chat with full bubble styling and a thread panel with minimal styling), use Angular’s hierarchical dependency injection to scope the service:
import { Component, inject, AfterViewInit, TemplateRef, ViewChild, Input } from '@angular/core';import { CometChat } from '@cometchat/chat-sdk-javascript';import { CometChatMessageListComponent, MessageBubbleConfigService, FormatterConfigService,} from '@cometchat/chat-uikit-angular';@Component({ selector: 'app-thread-panel', standalone: true, imports: [CometChatMessageListComponent], // These create NEW instances scoped to this component and its children providers: [MessageBubbleConfigService, FormatterConfigService], template: ` <cometchat-message-list [user]="user" [group]="group" [parentMessageId]="parentMessageId" ></cometchat-message-list> `,})export class ThreadPanelComponent implements AfterViewInit { @Input() user?: CometChat.User; @Input() group?: CometChat.Group; @Input() parentMessageId?: number; // Injects the LOCAL instance, not the root singleton private bubbleConfig = inject(MessageBubbleConfigService); ngAfterViewInit(): void { // Customizations here only affect the thread panel }}
The main panel’s customizations (set on the root singleton) do not affect the thread panel, and vice versa.Services you can scope this way:
Service
What it customizes
MessageBubbleConfigService
Bubble templates per message type
FormatterConfigService
Text formatters (mentions, URLs, custom)
CometChatTemplatesService
Shared and component-specific list templates (loading, empty, error, item views)
RichTextEditorService
Rich text editor configuration
Do not scope ChatStateService. It is intentionally a singleton that tracks the app-wide active conversation. Scoping it would break cross-component state synchronization. Use the props-based pattern instead for independent panels.