cometchat-react-router-patterns
Purpose
This skill teaches Claude how to integrate CometChat into React Router projects. React Router exists in two distinct modes with very different integration patterns:
- v6 library mode: React Router is used as a routing library in a Vite/CRA app. No SSR. Simple.
- v7 framework mode: React Router is a full framework (successor to Remix) with file-system routing, loaders, actions, and SSR. Complex -- similar SSR concerns as Next.js.
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)
1. Project detection
Identifying React Router
# Check package.json for React Router
grep -E '"react-router-dom"|"react-router"' package.json
Distinguishing v6 library mode from v7 framework mode
# v7 framework mode: has a config file
ls react-router.config.ts react-router.config.js 2>/dev/null
# v6 library mode: uses createBrowserRouter or <BrowserRouter> in source
grep -rn "createBrowserRouter\|BrowserRouter\|<Routes" src/ --include="*.tsx" --include="*.jsx" 2>/dev/null | head -5
Decision:
- If
react-router.config.tsorreact-router.config.jsexists: v7 framework mode - If
react-router-domis inpackage.jsonwithcreateBrowserRouteror<BrowserRouter>in source: v6 library mode - If
react-router(notreact-router-dom) is inpackage.jsonwithout a config file: likely v7 in library mode -- treat as v6 library mode
2. v6 library mode patterns
v6 library mode is essentially a plain React app with routing. There are no SSR concerns. CometChat integration follows the same patterns as the cometchat-react-patterns skill, with routing-specific additions.
CometChatProvider placement
Mount the provider at the router level, wrapping the entire router or a specific route subtree:
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { CometChatProvider } from "./providers/CometChatProvider";
import { router } from "./router";
import "@cometchat/chat-uikit-react/css-variables.css";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<CometChatProvider>
<RouterProvider router={router} />
</CometChatProvider>
</React.StrictMode>
);
The provider implementation is the same as in cometchat-react-patterns section 2 -- uses import.meta.env.VITE_COMETCHAT_* for env vars.
Adding a route with createBrowserRouter
Find the router configuration and add a new route:
// src/router.tsx
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 /> },
// ... existing routes
],
},
]);
Adding a route with JSX Routes
// In App.tsx
import { Routes, Route } from "react-router-dom";
import Layout from "./components/Layout";
import ChatPage from "./pages/ChatPage";
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<HomePage />} />
<Route path="messages" element={<ChatPage />} />
</Route>
</Routes>
);
}
Nested conversation routes with Outlet
A powerful pattern for React Router: use nested routes to show conversation details alongside the conversation list.
// Router config
export const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{
path: "messages",
element: <ChatLayout />,
children: [
{ index: true, element: <EmptyState /> },
{ path: ":conversationId", element: <ConversationView /> },
],
},
],
},
]);
// ChatLayout.tsx -- renders conversation list + Outlet for message view
import { Outlet } from "react-router-dom";
import { CometChatConversations } from "@cometchat/chat-uikit-react";
import { useNavigate } from "react-router-dom";
export default function ChatLayout() {
const navigate = useNavigate();
return (
<div style={{ display: "flex", height: "100vh" }}>
<div style={{ width: "360px", borderRight: "1px solid #eee" }}>
<CometChatConversations
onItemClick={(conversation) => {
const id = conversation.getConversationId();
navigate(`/messages/${id}`);
}}
/>
</div>
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
<Outlet />
</div>
</div>
);
}
// ConversationView.tsx -- renders messages for the selected conversation
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import {
CometChatMessageHeader,
CometChatMessageList,
CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";
export default function ConversationView() {
const { conversationId } = useParams();
const [user, setUser] = useState<CometChat.User>();
const [group, setGroup] = useState<CometChat.Group>();
useEffect(() => {
if (!conversationId) return;
// Conversation IDs follow the pattern: "user_<uid>" or "group_<guid>"
if (conversationId.startsWith("user_")) {
const uid = conversationId.replace("user_", "");
CometChat.getUser(uid).then((u) => {
setUser(u);
setGroup(undefined);
});
} else if (conversationId.startsWith("group_")) {
const guid = conversationId.replace("group_", "");
CometChat.getGroup(guid).then((g) => {
setUser(undefined);
setGroup(g);
});
}
}, [conversationId]);
if (!user && !group) return null;
return (
<>
{user && <CometChatMessageHeader user={user} />}
{group && <CometChatMessageHeader group={group} />}
{user && <CometChatMessageList user={user} />}
{group && <CometChatMessageList group={group} />}
{user && <CometChatMessageComposer user={user} />}
{group && <CometChatMessageComposer group={group} />}
</>
);
}
useNavigate inside CometChat callbacks
CometChat's onItemClick callbacks can use React Router's useNavigate to drive URL changes:
import { useNavigate } from "react-router-dom";
function ConversationsList() {
const navigate = useNavigate();
return (
<CometChatConversations
onItemClick={(conversation) => {
const entity = conversation.getConversationWith();
if (entity instanceof CometChat.User) {
navigate(`/messages/user_${entity.getUid()}`);
} else if (entity instanceof CometChat.Group) {
navigate(`/messages/group_${entity.getGuid()}`);
}
}}
/>
);
}
This works because useNavigate returns a stable function that CometChat's callback invokes in the browser.
3. v7 framework mode patterns
React Router v7 in framework mode is a full meta-framework with SSR, file-system routing, loaders, and actions. It has the same SSR concerns as Next.js: CometChat components cannot run on the server.
SSR prevention
v7 renders components on the server by default. CometChat components will crash during server rendering. Use a ClientOnly wrapper:
// app/components/ClientOnly.tsx
import { useState, useEffect, type ReactNode } from "react";
interface ClientOnlyProps {
children: ReactNode;
fallback?: ReactNode;
}
export function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted ? <>{children}</> : <>{fallback}</>;
}
Or use React.lazy + Suspense:
import { lazy, Suspense } from "react";
const ChatView = lazy(() => import("../components/ChatView"));
export default function MessagesRoute() {
return (
<ClientOnly fallback={<div>Loading chat...</div>}>
<Suspense fallback={<div>Loading chat...</div>}>
<ChatView />
</Suspense>
</ClientOnly>
);
}
CometChatProvider for v7 framework mode
The provider needs ClientOnly wrapping because useEffect runs on the client but the module import of @cometchat/chat-uikit-react happens on the server too:
// app/providers/CometChatProvider.tsx
import React, { useEffect, useState, createContext, useContext } from "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. Without the loginInFlight guard, a second mount calls
// login() while the first is still pending and the SDK throws
// "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;
return;
}
loginInFlight = authToken
? CometChatUIKit.loginWithAuthToken(authToken)
: CometChatUIKit.login(uid);
try {
await loginInFlight;
} finally {
loginInFlight = null;
}
}
export function CometChatProvider({ children }: { children: React.ReactNode }) {
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function setup() {
try {
// Dynamic import to prevent server-side module resolution
const { CometChatUIKit, UIKitSettingsBuilder } = await import(
"@cometchat/chat-uikit-react"
);
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>
);
}
Key difference from the plain React provider: The import("@cometchat/chat-uikit-react") is done dynamically inside useEffect, NOT at the top level. This prevents the server from trying to resolve the module.
Mount the provider
Option A — Chat-only app (all routes use CometChat):
Wrap the entire app. This disables SSR for all routes since ClientOnly renders nothing on the server.
// app/root.tsx
import { Outlet } from "react-router";
import { ClientOnly } from "./components/ClientOnly";
import { CometChatProvider } from "./providers/CometChatProvider";
export default function Root() {
return (
<html lang="en">
<body>
<ClientOnly>
<CometChatProvider>
<Outlet />
</CometChatProvider>
</ClientOnly>
</body>
</html>
);
}
Option B — Mixed app (some routes need SSR):
Keep the root clean and scope CometChat to a layout route. Non-chat routes keep full SSR.
// app/root.tsx — no CometChat here, SSR works normally
import { Outlet } from "react-router";
export default function Root() {
return (
<html lang="en">
<body>
<Outlet />
</body>
</html>
);
}
// app/routes/chat.tsx — layout route for all /chat/* paths
import { Outlet } from "react-router";
import { ClientOnly } from "../components/ClientOnly";
import { CometChatProvider } from "../providers/CometChatProvider";
export default function ChatLayout() {
return (
<ClientOnly fallback={<div>Loading chat...</div>}>
<CometChatProvider>
<Outlet />
</CometChatProvider>
</ClientOnly>
);
}
Then nest chat routes under this layout (e.g. app/routes/chat.messages.tsx). Marketing pages, dashboards, and other non-chat routes render server-side as normal.
Use Option A for apps where every page involves chat (messaging apps, social platforms). Use Option B for apps that add chat to an existing product (SaaS, marketplaces, support).
File-system routing
v7 framework mode uses file-based routing. Create a route file:
// app/routes/messages.tsx
import { lazy, Suspense } from "react";
import { ClientOnly } from "../components/ClientOnly";
const ChatView = lazy(() => import("../components/ChatView"));
export default function MessagesRoute() {
return (
<ClientOnly fallback={<div>Loading chat...</div>}>
<Suspense fallback={<div>Loading chat...</div>}>
<ChatView />
</Suspense>
</ClientOnly>
);
}
Loaders and actions
NEVER put CometChat initialization or SDK calls in a loader or action. Loaders and actions run on the server in v7 framework mode. CometChat requires browser APIs.
// WRONG -- loader runs on the server
export async function loader() {
const { CometChat } = await import("@cometchat/chat-sdk-javascript");
const user = await CometChat.getUser("uid"); // crashes on server
return { user };
}
// CORRECT -- use clientLoader if you need chat data at route entry
export async function clientLoader() {
const { CometChat } = await import("@cometchat/chat-sdk-javascript");
const user = await CometChat.getUser("uid");
return { user };
}
clientLoader runs only in the browser. It is the right place for CometChat data fetching at the route level. However, most CometChat integrations do not need loaders at all -- the components handle their own data fetching.
4. Outlet patterns for nested conversations
This pattern works in both v6 and v7. Chat as a parent route with nested child routes:
/messages → Conversation list (left pane), empty state (right pane)
/messages/:conversationId → Conversation list (left pane), messages (right pane)
v6 route config
{
path: "messages",
element: <ChatLayout />,
children: [
{ index: true, element: <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#999" }}>Select a conversation</div> },
{ path: ":conversationId", element: <ConversationView /> },
],
}
v7 file-system routes
app/routes/
messages.tsx → ChatLayout (renders <Outlet />)
messages._index.tsx → Empty state
messages.$conversationId.tsx → ConversationView
// app/routes/messages.tsx
import { Outlet } from "react-router";
import { ClientOnly } from "../components/ClientOnly";
import { lazy, Suspense } from "react";
const ConversationList = lazy(() => import("../components/ConversationList"));
export default function MessagesLayout() {
return (
<div style={{ display: "flex", height: "100vh" }}>
<div style={{ width: "360px", borderRight: "1px solid #eee" }}>
<ClientOnly>
<Suspense fallback={<div>Loading...</div>}>
<ConversationList />
</Suspense>
</ClientOnly>
</div>
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
<Outlet />
</div>
</div>
);
}
5. Environment variables
v6 library mode (Vite-based)
Same as cometchat-react-patterns -- uses VITE_ prefix:
VITE_COMETCHAT_APP_ID=your_app_id
VITE_COMETCHAT_REGION=us
VITE_COMETCHAT_AUTH_KEY=your_auth_key
Access: import.meta.env.VITE_COMETCHAT_APP_ID
v7 framework mode
Also uses Vite under the hood, so the same VITE_ prefix applies:
VITE_COMETCHAT_APP_ID=your_app_id
VITE_COMETCHAT_REGION=us
VITE_COMETCHAT_AUTH_KEY=your_auth_key
Server-only variables (for auth token generation in actions) can omit the VITE_ prefix so they are never exposed to the client bundle:
COMETCHAT_AUTH_TOKEN=your_server_secret
Access server-only vars in loaders/actions via process.env:
// app/routes/api.cometchat-token.tsx (runs server-side only)
export async function action({ request }: ActionFunctionArgs) {
const secret = process.env.COMETCHAT_AUTH_TOKEN;
// ...
}
Note:
process.envis available in loaders/actions when using the default Node adapter (@react-router/node). Other adapters (Cloudflare, Deno) expose env differently — check your adapter's docs.
6. CSS import
v6 library mode
Import in src/main.tsx:
import "@cometchat/chat-uikit-react/css-variables.css";
v7 framework mode
Import in app/root.tsx:
import "@cometchat/chat-uikit-react/css-variables.css";
Or use the links export:
// app/root.tsx
import cometchatStyles from "@cometchat/chat-uikit-react/css-variables.css?url";
export function links() {
return [{ rel: "stylesheet", href: cometchatStyles }];
}
The ?url suffix tells Vite to return a URL instead of injecting the CSS, which works with React Router's links convention.
7. Common pitfalls
Loaders are server-side in v7
The most common mistake in v7 framework mode. Loaders and actions run on the server. Any CometChat code in a loader crashes with window is not defined. Use clientLoader instead, or handle data fetching in the component.
v7's unstable_middleware
React Router v7's unstable_middleware feature does NOT affect CometChat. CometChat has no middleware requirements. Do not add CometChat-related middleware.
useNavigate in CometChat callbacks
useNavigate works inside CometChat's event callbacks (onItemClick, etc.) because these callbacks execute in the browser within the React tree. This is safe:
const navigate = useNavigate();
<CometChatConversations
onItemClick={(conv) => navigate(`/messages/${conv.getConversationId()}`)}
/>
Don't init in loaders
Even clientLoader is not the right place for CometChat initialization. Init should happen once in the provider (app root), not per-route. clientLoader is only appropriate for CometChat data queries (like CometChat.getUser()) that need to complete before the route renders.
v6 to v7 migration
If a project is migrating from v6 library mode to v7 framework mode, the CometChat integration must change:
- Add
ClientOnlywrapper - Move from static imports to dynamic imports for CometChat modules
- Move from
createBrowserRouterroutes to file-system routes - Add SSR guards
Do not mix v6 and v7 patterns. Detect the mode (section 1) and use the correct pattern.
8. Complete integration checklist (v6 library mode)
- Install packages:
npm install @cometchat/chat-uikit-react @cometchat/chat-sdk-javascript - Create
.envwithVITE_COMETCHAT_APP_ID,VITE_COMETCHAT_REGION,VITE_COMETCHAT_AUTH_KEY - Add
.envto.gitignore - Import
@cometchat/chat-uikit-react/css-variables.cssinsrc/main.tsx - Create
src/providers/CometChatProvider.tsx(usesimport.meta.env.VITE_*) - Mount
CometChatProviderinsrc/main.tsxwrapping<RouterProvider> - Create
src/pages/ChatPage.tsx(seecometchat-placementfor patterns) - Add
{ path: "messages", element: <ChatPage /> }to the router config - Add a "Messages" link to the layout's nav
9. Complete integration checklist (v7 framework mode)
- Install packages:
npm install @cometchat/chat-uikit-react @cometchat/chat-sdk-javascript - Create
.envwithVITE_COMETCHAT_APP_ID,VITE_COMETCHAT_REGION,VITE_COMETCHAT_AUTH_KEY - Add
.envto.gitignore - Import
@cometchat/chat-uikit-react/css-variables.cssinapp/root.tsx - Create
app/components/ClientOnly.tsx(section 3) - Create
app/providers/CometChatProvider.tsxwith dynamic imports (section 3) - Mount
ClientOnly+CometChatProviderinapp/root.tsx - Create
app/routes/messages.tsxwithClientOnly+ lazy import (section 3) - Add a "Messages" link to the layout's nav
- Verify: loaders/actions contain NO CometChat imports