Skip to main content

Overview

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:
  1. Service-based (default) — Components automatically subscribe to ChatStateService and react when the active entity changes. No prop passing required.
  2. 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.

Decision Matrix

Use this table to decide which pattern fits your use case:
CriteriaService-BasedProps-Based
Setup complexityZero config — components auto-subscribeManual — you manage and pass state
BoilerplateMinimalMore wiring code
Multi-panel layoutsNot ideal — single active entityRequired — each panel gets its own entity
Component isolationShared state across all instancesEach instance can have independent state
When to useSingle chat view, standard layoutsMulti-panel, embedded widgets, testing
State syncAutomatic across all Chat-Aware ComponentsYou control sync manually
Recommended forMost applicationsAdvanced 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.

Service-Based Pattern

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.
import { Component, inject } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import {
  CometChatConversationsComponent,
  CometChatMessageHeaderComponent,
  CometChatMessageListComponent,
  CometChatMessageComposerComponent,
  ChatStateService,
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-chat',
  standalone: true,
  imports: [
    CometChatConversationsComponent,
    CometChatMessageHeaderComponent,
    CometChatMessageListComponent,
    CometChatMessageComposerComponent,
  ],
  template: `
    <div class="chat-layout">
      <aside class="sidebar">
        <cometchat-conversations></cometchat-conversations>
      </aside>

      @if (chatStateService.activeUser() || chatStateService.activeGroup()) {
        <main class="chat-panel">
          <cometchat-message-header></cometchat-message-header>
          <cometchat-message-list></cometchat-message-list>
          <cometchat-message-composer></cometchat-message-composer>
        </main>
      } @else {
        <main class="chat-panel empty">
          <p>Select a conversation to start chatting</p>
        </main>
      }
    </div>
  `,
  styles: [`
    .chat-layout { display: flex; height: 100vh; }
    .sidebar { width: 360px; border-right: 1px solid #e0e0e0; }
    .chat-panel { flex: 1; display: flex; flex-direction: column; }
    .chat-panel.empty { align-items: center; justify-content: center; }
  `]
})
export class ChatComponent {
  chatStateService = inject(ChatStateService);
}
In this example:
  • <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.

Props-Based Pattern

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.
import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import {
  CometChatConversationsComponent,
  CometChatMessageHeaderComponent,
  CometChatMessageListComponent,
  CometChatMessageComposerComponent,
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-chat',
  standalone: true,
  imports: [
    CometChatConversationsComponent,
    CometChatMessageHeaderComponent,
    CometChatMessageListComponent,
    CometChatMessageComposerComponent,
  ],
  template: `
    <div class="chat-layout">
      <aside class="sidebar">
        <cometchat-conversations
          (itemClick)="onConversationClick($event)"
        ></cometchat-conversations>
      </aside>

      @if (selectedUser) {
        <main class="chat-panel">
          <cometchat-message-header [user]="selectedUser"></cometchat-message-header>
          <cometchat-message-list [user]="selectedUser"></cometchat-message-list>
          <cometchat-message-composer [user]="selectedUser"></cometchat-message-composer>
        </main>
      }

      @if (selectedGroup) {
        <main class="chat-panel">
          <cometchat-message-header [group]="selectedGroup"></cometchat-message-header>
          <cometchat-message-list [group]="selectedGroup"></cometchat-message-list>
          <cometchat-message-composer [group]="selectedGroup"></cometchat-message-composer>
        </main>
      }
    </div>
  `,
  styles: [`
    .chat-layout { display: flex; height: 100vh; }
    .sidebar { width: 360px; border-right: 1px solid #e0e0e0; }
    .chat-panel { flex: 1; display: flex; flex-direction: column; }
  `]
})
export class ChatComponent {
  selectedUser: CometChat.User | null = null;
  selectedGroup: CometChat.Group | null = null;

  onConversationClick(conversation: CometChat.Conversation): void {
    const conversationWith = conversation.getConversationWith();

    if (conversationWith instanceof CometChat.User) {
      this.selectedUser = conversationWith;
      this.selectedGroup = null;
    } else if (conversationWith instanceof CometChat.Group) {
      this.selectedGroup = conversationWith;
      this.selectedUser = null;
    }
  }
}
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.

Mutual Exclusivity

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.

Advanced Patterns

Multi-Panel Chat Layouts

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:
import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import {
  CometChatMessageHeaderComponent,
  CometChatMessageListComponent,
  CometChatMessageComposerComponent,
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-multi-panel',
  standalone: true,
  imports: [
    CometChatMessageHeaderComponent,
    CometChatMessageListComponent,
    CometChatMessageComposerComponent,
  ],
  template: `
    <div class="multi-panel-layout">
      <!-- Panel 1: Direct message with a user -->
      @if (userA) {
        <div class="chat-panel">
          <cometchat-message-header [user]="userA"></cometchat-message-header>
          <cometchat-message-list [user]="userA"></cometchat-message-list>
          <cometchat-message-composer [user]="userA"></cometchat-message-composer>
        </div>
      }

      <!-- Panel 2: Group conversation -->
      @if (teamGroup) {
        <div class="chat-panel">
          <cometchat-message-header [group]="teamGroup"></cometchat-message-header>
          <cometchat-message-list [group]="teamGroup"></cometchat-message-list>
          <cometchat-message-composer [group]="teamGroup"></cometchat-message-composer>
        </div>
      }
    </div>
  `,
  styles: [`
    .multi-panel-layout { display: flex; height: 100vh; gap: 1px; }
    .chat-panel { flex: 1; display: flex; flex-direction: column; border: 1px solid #e0e0e0; }
  `]
})
export class MultiPanelComponent {
  userA: CometChat.User | null = null;
  teamGroup: CometChat.Group | null = null;

  async ngOnInit(): Promise<void> {
    // Fetch specific entities for each panel
    this.userA = await CometChat.getUser('alice');
    this.teamGroup = await CometChat.getGroup('team-engineering');
  }
}
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.

Programmatic Navigation

Use ChatStateService to programmatically switch the active chat — for example, when navigating from a notification or deep link:
import { Component, inject } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { ChatStateService } from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-notification-handler',
  standalone: true,
  template: `<button (click)="openChat()">Open Chat</button>`
})
export class NotificationHandlerComponent {
  private chatStateService = inject(ChatStateService);

  async openChat(): Promise<void> {
    // Fetch the user from a notification payload
    const user: CometChat.User = await CometChat.getUser('alice');

    // Set as active — all subscribed components update automatically
    this.chatStateService.setActiveUser(user);
  }

  async openGroupChat(groupId: string): Promise<void> {
    const group: CometChat.Group = await CometChat.getGroup(groupId);
    this.chatStateService.setActiveGroup(group);
  }
}

Cleanup via clearActiveChat()

Call clearActiveChat() to reset all active state when the user logs out, navigates away from the chat interface, or closes a chat window:
import { Component, inject, OnDestroy } from '@angular/core';
import { ChatStateService } from '@cometchat/chat-uikit-angular';
import { CometChat } from '@cometchat/chat-sdk-javascript';

@Component({
  selector: 'app-chat-container',
  standalone: true,
  template: `
    <div class="chat-container">
      <button (click)="logout()">Logout</button>
      <!-- Chat components here -->
    </div>
  `
})
export class ChatContainerComponent implements OnDestroy {
  private chatStateService = inject(ChatStateService);

  logout(): void {
    // Clear all chat state before logging out
    this.chatStateService.clearActiveChat();
    CometChat.logout();
  }

  ngOnDestroy(): void {
    // Clean up when navigating away from the chat view
    this.chatStateService.clearActiveChat();
  }
}
Always call clearActiveChat() during logout to ensure no stale state persists across sessions.

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:
ServiceWhat it customizes
MessageBubbleConfigServiceBubble templates per message type
FormatterConfigServiceText formatters (mentions, URLs, custom)
CometChatTemplatesServiceShared and component-specific list templates (loading, empty, error, item views)
RichTextEditorServiceRich 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.
See CometChatMessageList — Multiple Message Lists with Different Configurations for a complete example with two panels.