| 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 |
| metadata | {"author":"CometChat","version":"3.0.0","tags":"chat cometchat react vite cra patterns provider routing integration"} |
Ground truth: @cometchat/chat-uikit-react@^6 (+ @cometchat/calls-sdk-javascript@^5) — installed package types + ui-kit/react. Official docs: https://www.cometchat.com/docs/ui-kit/react/overview · Docs MCP: claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp (or fetch the URL directly without MCP). Verify symbols against the installed package/source before relying on them.
Purpose
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-patterns
cometchat-components -- component catalog and composition patterns
cometchat-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.
1. Project detection
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 instead
astro -- 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 instead
Edge 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:
cat package.json | grep -E '"(vite|react-scripts|next|astro|@remix-run)"'
2. CometChatProvider for React
React (Vite/CRA) is client-only, so there are no SSR concerns. The provider can be simple.
Full implementation
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);
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 && existing.getUid?.() === uid) return;
if (existing) await CometChatUIKit.logout();
if (loginInFlight) {
await loginInFlight;
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");
setIsReady(true);
} catch (e) {
setError(formatCometChatError(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>
);
}
Where to mount
Mount CometChatProvider in the app's entry point, wrapping either the entire app or the router:
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.
Production login variant
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.
3. Routing integration
React projects handle routing in different ways. Detect which pattern the project uses, then integrate accordingly.
How to detect the routing pattern
grep -r "react-router-dom" package.json
grep -rn "createBrowserRouter\|BrowserRouter\|<Routes" src/ --include="*.tsx" --include="*.jsx" 2>/dev/null | head -5
Pattern A: React Router v6 with createBrowserRouter
This is the modern recommended pattern. The router is defined as a data structure.
import { createBrowserRouter } from "react-router-dom";
import Layout from "./components/Layout";
import HomePage from "./pages/HomePage";
import ChatPage from "./pages/ChatPage";
export const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{ index: true, element: <HomePage /> },
{ path: "messages", element: <ChatPage /> },
],
},
]);
Pattern B: React Router v6 with JSX Routes
Older pattern using <Routes> and <Route> elements:
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>
);
}
Pattern C: No router (single-page app)
Some React projects have no router at all. Chat is shown conditionally:
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.
Full chat page component
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.
4. Layout integration
Finding the layout
Read the project's source to find the component that renders the navigation. Common locations:
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 children
src/layouts/MainLayout.tsx or src/layouts/AppLayout.tsx
src/components/Navbar.tsx or src/components/Header.tsx
Adding a navigation link
Once you find the nav, add a "Messages" link alongside existing links. Match the existing style:
import { Link } from "react-router-dom";
<Link to="/messages">Messages</Link>
import { NavLink } from "react-router-dom";
<NavLink to="/messages" className={({ isActive }) => isActive ? "active" : ""}>
Messages
</NavLink>
Adding a chat drawer/modal trigger
If the placement is a drawer or modal instead of (or in addition to) a route, add a trigger button to the nav:
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.
5. Environment variables
Vite projects
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
CRA projects
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).
TypeScript type hints (Vite only)
For better IDE support, add CometChat env vars to src/vite-env.d.ts:
interface ImportMetaEnv {
readonly VITE_COMETCHAT_APP_ID: string;
readonly VITE_COMETCHAT_REGION: string;
readonly VITE_COMETCHAT_AUTH_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Production auth keys
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:
- A separate backend (Express, Fastify, etc.)
- A serverless function (Vercel, Netlify, AWS Lambda)
- A BaaS like Firebase or Supabase with a Cloud Function
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.
6. CSS import
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):
import "@cometchat/chat-uikit-react/css-variables.css";
import "./index.css";
Alternatively, use a CSS @import in your root stylesheet:
@import "@cometchat/chat-uikit-react/css-variables.css";
Import order matters
CometChat's css-variables.css defines :root CSS custom properties. Your overrides must come AFTER this import to take effect:
@import "@cometchat/chat-uikit-react/css-variables.css";
:root {
--cometchat-primary-color: #6851d6;
}
:root {
--cometchat-primary-color: #6851d6;
}
@import "@cometchat/chat-uikit-react/css-variables.css";
7. Common pitfalls
StrictMode double-init
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.
CSS import duplication
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.
Hot Module Replacement (HMR)
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).
Container height (and the flex-shrink trap that breaks chat layouts)
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.
Bug 3 — list won't scroll at all even with a bounded parent: the height chain looks right but the list still grows/clips instead of scrolling. Cause: the kit renders a .cometchat flex element as the immediate child inside your list wrapper, and CometChatMessageList's own height: 100% only resolves if that injected element also has a height. Inline styles can't target an auto-injected child, so you MUST add a real CSS child rule. This is the #1 cause of "I set height everywhere and it still won't scroll."
The full height chain must reach a definite height — html, body { height: 100% } and #root { height: 100vh } (NOT min-height — min-height lets the container grow with content, so the list never gets a bound to scroll within). Vite/CRA starter CSS ships #root { min-height: 100vh; max-width: 1280px; ... } — delete it.
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.
<div style={{ height: "100vh" }}>
<CometChatConversations ... />
</div>
<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>
<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>
<div>
<CometChatConversations ... />
</div>
<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 kit-injected .cometchat wrapper (the part inline styles can't fix). CometChatMessageList renders a .cometchat flex element as its outermost node, with .cometchat-message-list { height: 100% } inside it. If you wrap the list in your own div, that injected .cometchat sits between your div and the list — and a percentage height: 100% resolves to auto against an auto-height parent, so the list grows/clips instead of scrolling. Give the list its own wrapper class and add a child rule (this is exactly what the kit's own sample app does — .cometchat-message-list-wrapper > .cometchat):
<div style={{ display: "flex", flexDirection: "column", height: "100%", minHeight: 0 }}>
<CometChatMessageHeader user={user} />
<div className="cc-msg-list">
<CometChatMessageList user={user} />
</div>
<CometChatMessageComposer user={user} />
</div>
.cc-msg-list { flex: 1 1 0; min-height: 0; height: 100%; overflow: hidden; }
.cc-msg-list > .cometchat { height: 100%; overflow: hidden; }
⚠️ The height: 100% on .cc-msg-list is non-negotiable and easy to miss. A wrapper sized purely by flex: 1 1 0 measures correctly (e.g. 645px) but Chromium still treats that height as indefinite for percentage-height children, so .cometchat falls back to content height (e.g. 1014px) and the messages clip silently with no scrollbar. This is the #1 "I followed the recipe and it still won't scroll" trap. The column that contains .cc-msg-list must itself have a definite height (the height: 100% + minHeight: 0 flex column above, anchored to a definite-height #root).
The rule, restated as something to grep for:
Every flex container that holds CometChatMessageList (directly or indirectly) MUST have minHeight: 0. Same for minWidth: 0 on horizontal flex parents. AND when you wrap the list, add a .your-list-wrapper > .cometchat { height: 100%; overflow: hidden } CSS rule for the kit's injected child — without it the list won't scroll no matter how many heights you set inline. The full chain (html/body/#root) must use a definite height, never min-height.
Vite dependency optimization
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:
export default defineConfig({
optimizeDeps: {
include: [
"@cometchat/chat-uikit-react",
"@cometchat/chat-sdk-javascript",
],
},
});
8. Complete integration checklist
When integrating CometChat into a React (Vite/CRA) project, follow these steps in order:
- Install packages:
npm install @cometchat/chat-uikit-react@^6 @cometchat/chat-sdk-javascript@^4 — ⚠️ keep the @^6 major pin; never install bare. v7 is on npm — a bare npm install @cometchat/chat-uikit-react pulls it once it's tagged latest, and these v6 skills break against the v7 API.
- Create
.env with VITE_COMETCHAT_APP_ID, VITE_COMETCHAT_REGION, VITE_COMETCHAT_AUTH_KEY
- Add
.env to .gitignore if not already there
- Import
@cometchat/chat-uikit-react/css-variables.css in src/main.tsx
- Create
src/providers/CometChatProvider.tsx (section 2)
- Mount
CometChatProvider in src/main.tsx wrapping <App />
- Create the chat page component (see
cometchat-placement for patterns)
- Wire the route (section 3) or trigger (section 4)
- Add a "Messages" link to the existing nav
Do not skip step 6. The provider must wrap the app root so init happens once, regardless of which route or modal opens chat.
9. Visual Builder integration (v4.3)
If the customer picks Visually in dispatcher Step 3.1, the React-on-Vite/CRA recipe diverges from the code-driven path described above. Instead of authoring a CometChatProvider, skills runs cometchat builder export --platform react --json to download the canonical src/CometChat/ directory + patch CometChatSettings.ts with the customer's per-builder configuration. Then patches src/main.tsx (Vite) or src/index.tsx (CRA) to mount <CometChatApp />.
Full recipe lives in cometchat-core §11 "Visual Builder integration". This section is a pointer + Vite/CRA-specific gotchas:
cometchat builder export --platform react is the single command — replaces the v4.2 era "fetch JSON + manually copy" pattern. Defaults to --output src/CometChat. Resync = same command with --force. See cometchat-core §11.1.
- Vite 7+ tsconfig is auto-relaxed by
cometchat apply (Finding F35, 2026-05-22) — verbatimModuleSyntax, noUnusedLocals, noUnusedParameters, erasableSyntaxOnly all set to false. CRA's defaults are already permissive — no change needed for CRA. For Visually path, also set resolveJsonModule: true + allowJs: true per cometchat-core §11.2.
package.json needs cometChatCustomConfig block (Finding F2). The canonical context reads packageJson.cometChatCustomConfig.{name, version, production}.
- Pinned versions:
@cometchat/chat-uikit-react@6.4.3 + @cometchat/calls-sdk-javascript@4.2.5 (per the canonical README; newer versions may drift from the copied CometChat/ API surface).
If the customer picks In code, ignore this section — sections 1-8 above are the path. (cometchat apply auto-patches tsconfig for both paths per F35.)