with one click
cometchat-react-patterns
// Framework-specific patterns for integrating CometChat React UI Kit v6 into React projects (Vite or CRA). Covers provider setup, routing, layout integration, env vars, and common pitfalls.
// Framework-specific patterns for integrating CometChat React UI Kit v6 into React projects (Vite or CRA). Covers provider setup, routing, layout integration, env vars, and common pitfalls.
| name | cometchat-react-patterns |
| description | Framework-specific patterns for integrating CometChat React UI Kit v6 into React projects (Vite or CRA). Covers provider setup, routing, layout integration, env vars, and common pitfalls. |
| license | MIT |
| compatibility | Node.js >=18; React >=18; Vite >=4 or react-scripts (CRA); @cometchat/chat-uikit-react ^6; @cometchat/chat-sdk-javascript ^4 |
| allowed-tools | executeBash, readFile, fileSearch, listDirectory |
| metadata | {"author":"CometChat","version":"3.0.0","tags":"chat cometchat react vite cra patterns provider routing integration"} |
This skill teaches Claude how to integrate CometChat into a React project that uses Vite or Create React App. These are client-only environments with no SSR, which simplifies integration significantly.
Read these companion skills first:
cometchat-core -- initialization, login, CSS, provider pattern, anti-patternscometchat-components -- component catalog and composition patternscometchat-placement -- WHERE to put chat (route, modal, drawer, embedded)This skill covers the HOW for React specifically: project detection, provider wiring, routing patterns, env var conventions, and React-specific gotchas.
A project is a plain React project (not a meta-framework) when package.json has react and ONE of these bundlers, but NONE of the framework packages:
Bundler indicators (must have one):
vite in devDependencies (Vite project)react-scripts in dependencies (Create React App)Framework exclusions (must have none):
next -- use the cometchat-nextjs-patterns skill insteadastro -- use the cometchat-astro-patterns skill instead@remix-run/react or react-router (v7 with react-router.config.ts) -- use the cometchat-react-router-patterns skill insteadEdge case: If react-router-dom is present but next, astro, and @remix-run/react are absent, this IS a plain React project that uses React Router as a library. This skill applies.
Detection code:
# Quick check from the project root
cat package.json | grep -E '"(vite|react-scripts|next|astro|@remix-run)"'
React (Vite/CRA) is client-only, so there are no SSR concerns. The provider can be simple.
// src/providers/CometChatProvider.tsx
import React, { useEffect, useState, createContext, useContext } from "react";
import { CometChatUIKit, UIKitSettingsBuilder } from "@cometchat/chat-uikit-react";
interface CometChatContextValue {
isReady: boolean;
error: string | null;
}
const CometChatContext = createContext<CometChatContextValue>({
isReady: false,
error: null,
});
export const useCometChat = () => useContext(CometChatContext);
// Module-level state prevents both double-init AND double-login in React
// StrictMode. StrictMode mounts, unmounts, and remounts in development,
// which fires useEffect twice. Without guards, init runs twice (duplicate
// WebSocket connections) and login() is called a second time while the
// first is still in flight, which makes the SDK throw
// "Please wait until the previous login request ends."
let initialized = false;
let loginInFlight: Promise<unknown> | null = null;
async function ensureLoggedIn(
uid: string,
authToken?: string,
): Promise<void> {
const existing = await CometChatUIKit.getLoggedinUser();
if (existing) return;
if (loginInFlight) {
await loginInFlight; // a prior mount already started login — reuse its promise
return;
}
loginInFlight = authToken
? CometChatUIKit.loginWithAuthToken(authToken)
: CometChatUIKit.login(uid);
try {
await loginInFlight;
} finally {
loginInFlight = null;
}
}
interface CometChatProviderProps {
children: React.ReactNode;
}
export function CometChatProvider({ children }: CometChatProviderProps) {
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function setup() {
try {
if (!initialized) {
initialized = true;
const settings = new UIKitSettingsBuilder()
.setAppId(import.meta.env.VITE_COMETCHAT_APP_ID)
.setRegion(import.meta.env.VITE_COMETCHAT_REGION)
.setAuthKey(import.meta.env.VITE_COMETCHAT_AUTH_KEY)
.subscribePresenceForAllUsers()
.build();
await CometChatUIKit.init(settings);
}
await ensureLoggedIn("cometchat-uid-1"); // DEVELOPMENT ONLY — see cometchat-production skill
setIsReady(true);
} catch (e) {
setError(String(e));
}
}
setup();
}, []);
if (error) {
return (
<div style={{ color: "red", padding: 16, fontFamily: "monospace" }}>
CometChat Error: {error}
</div>
);
}
if (!isReady) return null;
return (
<CometChatContext.Provider value={{ isReady, error }}>
{children}
</CometChatContext.Provider>
);
}
Mount CometChatProvider in the app's entry point, wrapping either the entire app or the router:
// src/main.tsx (Vite)
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { CometChatProvider } from "./providers/CometChatProvider";
import "@cometchat/chat-uikit-react/css-variables.css";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<CometChatProvider>
<App />
</CometChatProvider>
</React.StrictMode>
);
For CRA, the pattern is identical but the file is src/index.tsx and uses createRoot the same way.
For production, replace the hardcoded login("cometchat-uid-1") with token-based auth. Fetch a token from your backend and use CometChatUIKit.loginWithAuthToken(token). See the cometchat-core skill, section 2 (Login), for the full production auth pattern.
React projects handle routing in different ways. Detect which pattern the project uses, then integrate accordingly.
# Check for React Router
grep -r "react-router-dom" package.json
# Check for router usage patterns
grep -rn "createBrowserRouter\|BrowserRouter\|<Routes" src/ --include="*.tsx" --include="*.jsx" 2>/dev/null | head -5
This is the modern recommended pattern. The router is defined as a data structure.
// src/router.tsx (or wherever the router is defined)
import { createBrowserRouter } from "react-router-dom";
import Layout from "./components/Layout";
import HomePage from "./pages/HomePage";
import ChatPage from "./pages/ChatPage"; // <-- new
export const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{ index: true, element: <HomePage /> },
{ path: "messages", element: <ChatPage /> }, // <-- add this route
// ... existing routes
],
},
]);
Older pattern using <Routes> and <Route> elements:
// Inside App.tsx or wherever routes are defined
import { Routes, Route } from "react-router-dom";
import ChatPage from "./pages/ChatPage";
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<HomePage />} />
<Route path="messages" element={<ChatPage />} /> {/* add this */}
</Route>
</Routes>
);
}
Some React projects have no router at all. Chat is shown conditionally:
// App.tsx
import { useState } from "react";
import ChatPage from "./pages/ChatPage";
function App() {
const [showChat, setShowChat] = useState(false);
if (showChat) {
return (
<div>
<button onClick={() => setShowChat(false)}>Back</button>
<ChatPage />
</div>
);
}
return (
<div>
{/* existing app content */}
<button onClick={() => setShowChat(true)}>Open Messages</button>
</div>
);
}
For projects without a router, consider suggesting react-router-dom if the project has multiple "pages." But do not force it -- some apps are intentionally single-page.
See the cometchat-placement skill for complete ChatPage implementations (two-pane, full messenger, single thread). The page component itself is framework-agnostic -- the React-specific part is only how the route is wired.
Read the project's source to find the component that renders the navigation. Common locations:
# Find likely layout/nav files
find src \( -name "*.tsx" -o -name "*.jsx" \) | xargs grep -l "nav\|Nav\|Sidebar\|Header" 2>/dev/null | head -10
Common patterns:
src/App.tsx with inline nav + <Outlet />src/components/Layout.tsx wrapping childrensrc/layouts/MainLayout.tsx or src/layouts/AppLayout.tsxsrc/components/Navbar.tsx or src/components/Header.tsxOnce you find the nav, add a "Messages" link alongside existing links. Match the existing style:
// If the project uses React Router's <Link>:
import { Link } from "react-router-dom";
// In the nav component, alongside existing links:
<Link to="/messages">Messages</Link>
// If the project uses React Router's <NavLink> for active styling:
import { NavLink } from "react-router-dom";
<NavLink to="/messages" className={({ isActive }) => isActive ? "active" : ""}>
Messages
</NavLink>
If the placement is a drawer or modal instead of (or in addition to) a route, add a trigger button to the nav:
// In the nav component
import { useState } from "react";
import { ChatDrawer } from "../components/ChatDrawer";
function Navbar() {
const [showChat, setShowChat] = useState(false);
return (
<nav>
{/* existing nav links */}
<button onClick={() => setShowChat(true)}>
Messages
</button>
<ChatDrawer isOpen={showChat} onClose={() => setShowChat(false)} />
</nav>
);
}
See cometchat-placement for the full ChatDrawer and ChatModal implementations.
Create a .env file in the project root:
VITE_COMETCHAT_APP_ID=your_app_id_here
VITE_COMETCHAT_REGION=us
VITE_COMETCHAT_AUTH_KEY=your_auth_key_here
Access in code: import.meta.env.VITE_COMETCHAT_APP_ID
Vite only exposes variables prefixed with VITE_ to client-side code. Variables without this prefix are server-only (available in vite.config.ts but not in components).
Important: Vite's .env is NOT gitignored by default. Add .env to .gitignore:
echo ".env" >> .gitignore
Create a .env file in the project root:
REACT_APP_COMETCHAT_APP_ID=your_app_id_here
REACT_APP_COMETCHAT_REGION=us
REACT_APP_COMETCHAT_AUTH_KEY=your_auth_key_here
Access in code: process.env.REACT_APP_COMETCHAT_APP_ID
CRA requires the REACT_APP_ prefix for client-side variables. CRA requires a restart after changing .env files (Vite does not).
For better IDE support, add CometChat env vars to src/vite-env.d.ts:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_COMETCHAT_APP_ID: string;
readonly VITE_COMETCHAT_REGION: string;
readonly VITE_COMETCHAT_AUTH_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
The AUTH_KEY is for development only. In production, you need a backend that generates auth tokens. Plain React (Vite/CRA) projects have no built-in server, so you need either:
The backend calls CometChat's REST API with your AUTH_TOKEN (server-side secret, never exposed to client) to generate per-user auth tokens. See the cometchat-core skill, section 2, for the flow.
Import CometChat's CSS variables exactly once, at the app root. For React (Vite/CRA), the right place is src/main.tsx (or src/index.tsx for CRA):
// src/main.tsx
import "@cometchat/chat-uikit-react/css-variables.css";
import "./index.css"; // your app's styles AFTER the CometChat import
Alternatively, use a CSS @import in your root stylesheet:
/* src/index.css */
@import "@cometchat/chat-uikit-react/css-variables.css";
/* your styles below */
CometChat's css-variables.css defines :root CSS custom properties. Your overrides must come AFTER this import to take effect:
/* CORRECT: overrides come after */
@import "@cometchat/chat-uikit-react/css-variables.css";
:root {
--cometchat-primary-color: #6851d6;
}
/* WRONG: overrides come before -- they will be overwritten */
:root {
--cometchat-primary-color: #6851d6;
}
@import "@cometchat/chat-uikit-react/css-variables.css";
React's StrictMode (used by default in Vite and CRA development mode) intentionally double-invokes effects. Without the module-level initialized flag in the provider, CometChatUIKit.init() runs twice. The second call may silently fail or create a second WebSocket connection.
The initialized flag in the provider pattern (section 2) handles this. Never remove it. It is not a hack -- it is the correct pattern for one-time SDK initialization in React.
If css-variables.css is imported in multiple files (e.g., both main.tsx and ChatPage.tsx), CSS custom properties are declared twice. This usually works but can cause issues with specificity if the imports are processed in different order by the bundler. Import exactly once at the root.
Vite's HMR replaces modules without a full page reload. CometChat's SDK holds a WebSocket connection that survives HMR. This is generally fine -- the SDK connection persists and chat keeps working.
However, if you change the provider file itself during development, HMR may re-execute the module. The initialized flag prevents double-init, but the WebSocket connection from the previous module instance may linger. If you see duplicate messages or connection issues during development, do a full page reload (Ctrl+Shift+R).
CometChat components fill 100% of their container. Two visual bugs to avoid:
Bug 1 — zero height: components render with zero height because the container has no explicit dimensions. Bug 2 — message list grows past the viewport: the list scrolls fine until it has too many messages, then pushes the composer below the fold. This is the classic flex-shrink trap.
The hard rule for two-pane / header+list+composer layouts: every flex container in the chain MUST have minHeight: 0 (and minWidth: 0 for horizontal flex). Without it, browsers default flex-children to min-height: auto (their intrinsic content size), so the list grows beyond the parent's bounds as messages accumulate.
/* ✓ CORRECT: explicit height for a single-component surface */
<div style={{ height: "100vh" }}>
<CometChatConversations ... />
</div>
/* ✓ CORRECT: header + list + composer with the flex-shrink trap fixed */
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<nav>...</nav>
<div style={{
flex: 1,
display: "flex",
flexDirection: "column",
minHeight: 0, // ← THIS IS THE HARD RULE — without it, list grows past the viewport
}}>
<div style={{ flex: "0 0 auto" }}>
<CometChatMessageHeader user={user} />
</div>
<div style={{ flex: "1 1 0", minHeight: 0, overflow: "hidden" }}>
<CometChatMessageList user={user} />
</div>
<div style={{ flex: "0 0 auto" }}>
<CometChatMessageComposer user={user} />
</div>
</div>
</div>
/* ✓ CORRECT: two-pane (sidebar + active chat) — both axes need min-{width,height}: 0 */
<div style={{ display: "flex", height: "100vh" }}>
<div style={{ width: 360, display: "flex", flexDirection: "column" }}>
<CometChatConversations onItemClick={...} />
</div>
<div style={{
flex: 1,
display: "flex",
flexDirection: "column",
minWidth: 0, // ← horizontal flex parent: prevents content from forcing horizontal scroll
minHeight: 0, // ← vertical flex (the inner column): prevents the list-overflow bug
}}>
{/* header / list / composer wrapped as above */}
</div>
</div>
/* ✗ WRONG: no height constraint — component collapses to zero */
<div>
<CometChatConversations ... />
</div>
/* ✗ WRONG: flex parent without minHeight: 0 — list grows past the viewport once
you accumulate more messages than fit on-screen. Composer falls off the bottom. */
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<CometChatMessageHeader user={user} />
<CometChatMessageList user={user} /> {/* takes intrinsic content height */}
<CometChatMessageComposer user={user} /> {/* gets pushed below the fold */}
</div>
Why minHeight: 0 is non-obvious: the W3C spec sets the default min-height of a flex item to auto (its intrinsic content height). For a scrollable list inside a flex column, this means the list refuses to shrink below its content even when the parent is bounded. Setting minHeight: 0 on the flex parent overrides this, letting the list shrink and scroll within the bounded container instead of overflowing it.
The rule, restated as something to grep for:
Every flex container that holds
CometChatMessageList(directly or indirectly) MUST haveminHeight: 0on it. Same forminWidth: 0on horizontal flex parents. Skip this and the layout works for short conversations and breaks for long ones.
Vite pre-bundles dependencies for faster dev startup. CometChat's packages are large and may trigger Vite's "new dependency found, reloading" message on first load. This is normal and only happens once. If it causes issues, you can pre-include the packages:
// vite.config.ts
export default defineConfig({
optimizeDeps: {
include: [
"@cometchat/chat-uikit-react",
"@cometchat/chat-sdk-javascript",
],
},
});
When integrating CometChat into a React (Vite/CRA) project, follow these steps in order:
npm install @cometchat/chat-uikit-react @cometchat/chat-sdk-javascript.env with VITE_COMETCHAT_APP_ID, VITE_COMETCHAT_REGION, VITE_COMETCHAT_AUTH_KEY.env to .gitignore if not already there@cometchat/chat-uikit-react/css-variables.css in src/main.tsxsrc/providers/CometChatProvider.tsx (section 2)CometChatProvider in src/main.tsx wrapping <App />cometchat-placement for patterns)Do not skip step 6. The provider must wrap the app root so init happens once, regardless of which route or modal opens chat.
[HINT] Download the complete skill directory including SKILL.md and all related files