cometchat-nextjs-patterns
$
npx mdskill add cometchat/cometchat-skills/cometchat-nextjs-patternsIntegrate CometChat React UI Kit into Next.js with SSR prevention.
- Prevents SSR errors by dynamically importing CometChat components with next/dynamic.
- Depends on @cometchat/chat-uikit-react ^6 and Next.js >=13.
- Chooses App Router or Pages Router patterns based on project structure.
- Delivers working chat UI with provider setup and API route examples.
SKILL.md
.github/skills/cometchat-nextjs-patternsView on GitHub ↗
---
name: cometchat-nextjs-patterns
description: "Framework-specific patterns for integrating CometChat React UI Kit v6 into Next.js projects (App Router and Pages Router). Covers SSR prevention, provider setup, route placement, API routes, and common pitfalls."
license: "MIT"
compatibility: "Node.js >=18; React >=18; Next.js >=13; @cometchat/chat-uikit-react ^6; @cometchat/chat-sdk-javascript ^4"
metadata:
author: "CometChat"
version: "3.0.0"
tags: "chat cometchat nextjs next react ssr app-router pages-router patterns"
---
> **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 Next.js project. Next.js is the most complex framework to integrate with because of Server-Side Rendering (SSR) and the Server Component / Client Component boundary. Every CometChat component is browser-only -- getting this wrong is the #1 source of integration failures.
**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 Next.js specifically.
---
## 1. Project detection
A project uses Next.js when `package.json` has `next` as a dependency.
### Detecting App Router vs Pages Router
Both may coexist in a project. Check which is primary:
```bash
# App Router: look for app/ directory with layout.tsx
ls app/layout.tsx app/layout.jsx 2>/dev/null
# Pages Router: look for pages/ directory with _app.tsx
ls pages/_app.tsx pages/_app.jsx pages/_app.js 2>/dev/null
```
**If `app/layout.tsx` exists, treat the project as App Router.** Even if `pages/` also exists, App Router is the primary routing mechanism in modern Next.js.
**If only `pages/` exists, treat the project as Pages Router.**
---
## 2. Critical: SSR prevention
**Every file that imports from `@cometchat/chat-uikit-react` MUST prevent server-side rendering.** CometChat components access `window`, `document`, and WebSocket APIs during import -- not just during render, but at import time. If Next.js tries to import these modules on the server, the build crashes with `ReferenceError: window is not defined`.
### App Router: "use client" directive
Add `"use client"` as the FIRST line of every file that imports CometChat:
```tsx
"use client";
import { CometChatConversations } from "@cometchat/chat-uikit-react";
// This file only runs in the browser
```
**Common mistake:** Putting `"use client"` AFTER imports. It must be the very first line, before any import statements.
```tsx
// WRONG -- "use client" is not the first line
import React from "react";
"use client"; // too late, has no effect
// CORRECT
"use client";
import React from "react";
```
### App Router: dynamic import from a Client Component (Next.js 15+ rule)
> ⚠️ **Next.js 15+ change (verified by runtime smoke 2026-06-02):** `dynamic(..., { ssr: false })` is **FORBIDDEN in Server Components** — Next.js 15+ throws a build error. The page that owns the dynamic-import MUST be a Client Component (`"use client"` at line 1). Otherwise the build fails with `Ecmascript file had an error` even if everything else is correct.
```tsx
// app/messages/page.tsx
"use client"; // ← REQUIRED in Next.js 15+ when using `dynamic({ ssr: false })`
import dynamic from "next/dynamic";
const ChatView = dynamic(() => import("../../components/ChatView"), {
ssr: false,
loading: () => <div>Loading chat...</div>,
});
export default function MessagesPage() {
return <ChatView />;
}
```
The `ChatView` component file itself must also have `"use client"` at the top. Both ends of the dynamic-import boundary need the directive.
**If you cannot make the page a Client Component** (because it needs server data-fetching), wrap your data-fetching in a separate Server Component sibling and pass results down via props to a Client Component that owns the CometChat subtree.
**Build still fails after applying both `"use client"` and `dynamic(... ssr:false)`?** Add `export const dynamic = "force-dynamic";` at the top of the page to opt out of static prerendering entirely (or `export const runtime = "edge";` for edge runtime). This is sometimes needed in Next.js 16 even with everything else correct — verified during 2026-06-02 smoke against Next.js 16.2.7. Skill-side suspicion: kit's module-eval-time `window` references still trip static prerender even via dynamic boundary.
### Pages Router: dynamic import
In the Pages Router, every page can potentially run on the server. Use `next/dynamic`:
```tsx
// pages/messages.tsx
import dynamic from "next/dynamic";
const ChatView = dynamic(() => import("../components/ChatView"), {
ssr: false,
loading: () => <div>Loading chat...</div>,
});
export default function MessagesPage() {
return <ChatView />;
}
```
---
## 3. CometChatProvider for Next.js (App Router)
### Full implementation
```tsx
// app/providers/CometChatProvider.tsx
"use client";
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. 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;
}
}
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(process.env.NEXT_PUBLIC_COMETCHAT_APP_ID!)
.setRegion(process.env.NEXT_PUBLIC_COMETCHAT_REGION!)
.setAuthKey(process.env.NEXT_PUBLIC_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(formatCometChatError(e)); // from cometchat-core §6 errors.ts — NOT String(e),
// which renders "[object Object]" on kit errors (ENG-35719)
}
}
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: Option A -- Global (chat available everywhere)
Wrap the entire app in `app/layout.tsx`. The layout itself is a Server Component, but the provider is a Client Component via `"use client"` in its file:
```tsx
// app/layout.tsx (Server Component)
import { CometChatProvider } from "./providers/CometChatProvider";
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<CometChatProvider>
{children}
</CometChatProvider>
</body>
</html>
);
}
```
**Note:** Importing a `"use client"` component from a Server Component is fine. Next.js renders the Server Component on the server and defers the Client Component to the browser. The `CometChatProvider` only runs its `useEffect` (and init) in the browser.
> ⚠️ **Option A REQUIRES `force-dynamic` on the layout (verified — real build, Next 16.2.9 + kit 6.5.1).** Mounting the provider in `app/layout.tsx` pulls the kit module into the layout's module graph, so `next build` evaluates it during the **static prerender** of *every* route — including `/` (which imports no CometChat code) and the framework-generated `/_not-found` — and crashes with `ReferenceError: window is not defined`. `"use client"` does NOT prevent this in Next 16. The per-page `export const dynamic = "force-dynamic"` from §"Build still fails…" cannot fix it, because `/_not-found` has no page file to annotate. **The fix is to put the directive on the LAYOUT, where it cascades to all routes including `/_not-found`:**
> ```tsx
> // app/layout.tsx — add this alongside the provider mount
> export const dynamic = "force-dynamic";
> ```
> If you would rather keep static prerendering for non-chat routes, use **Option B** (scoped route group) so `/` and `/_not-found` never import the kit — that is the cleaner choice for marketing/SSG-heavy sites.
### Where to mount: Option B -- Scoped (chat only on chat routes)
Use a route group to scope the provider to chat-related routes:
```
app/
layout.tsx <-- no CometChat here
page.tsx <-- home page, no chat overhead
(chat)/
layout.tsx <-- CometChatProvider wraps only this group
messages/
page.tsx <-- chat page
inbox/
page.tsx <-- another chat page
```
```tsx
// app/(chat)/layout.tsx
import { CometChatProvider } from "../providers/CometChatProvider";
export default function ChatLayout({ children }: { children: React.ReactNode }) {
return <CometChatProvider>{children}</CometChatProvider>;
}
```
Option B is better for performance: CometChat's SDK and WebSocket connection are only loaded when the user visits a chat route. Option A is simpler and ensures incoming call notifications work everywhere.
---
## 4. CometChatProvider for Next.js (Pages Router)
In the Pages Router, mount the provider in `_app.tsx`. Use dynamic import to prevent SSR:
```tsx
// pages/_app.tsx
import type { AppProps } from "next/app";
import dynamic from "next/dynamic";
import "../styles/globals.css";
const CometChatProvider = dynamic(
() => import("../components/CometChatProvider").then((mod) => mod.CometChatProvider),
{ ssr: false }
);
export default function App({ Component, pageProps }: AppProps) {
return (
<CometChatProvider>
<Component {...pageProps} />
</CometChatProvider>
);
}
```
The provider implementation is the same as section 3, but the file lives at `components/CometChatProvider.tsx` (no `"use client"` needed in Pages Router -- `ssr: false` handles SSR prevention).
---
## 5. Route placement (App Router)
Next.js App Router uses file-system routing. Creating a file at the right path automatically creates the route.
### Create the chat page
```tsx
// app/messages/page.tsx
"use client";
import { useState } from "react";
import {
CometChatConversations,
CometChatMessageHeader,
CometChatMessageList,
CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";
export default function MessagesPage() {
const [selectedUser, setSelectedUser] = useState<CometChat.User>();
const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();
function handleConversationClick(conversation: CometChat.Conversation) {
const entity = conversation.getConversationWith();
if (entity instanceof CometChat.User) {
setSelectedUser(entity);
setSelectedGroup(undefined);
} else if (entity instanceof CometChat.Group) {
setSelectedUser(undefined);
setSelectedGroup(entity);
}
}
return (
<div style={{ display: "flex", height: "100vh" }}>
<div style={{ width: "360px", borderRight: "1px solid #eee" }}>
<CometChatConversations onItemClick={handleConversationClick} />
</div>
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
{(selectedUser || selectedGroup) ? (
<>
{selectedUser && <CometChatMessageHeader user={selectedUser} />}
{selectedGroup && <CometChatMessageHeader group={selectedGroup} />}
{selectedUser && <CometChatMessageList user={selectedUser} hideReplyInThreadOption={true} />}
{selectedGroup && <CometChatMessageList group={selectedGroup} hideReplyInThreadOption={true} />}
{selectedUser && <CometChatMessageComposer user={selectedUser} />}
{selectedGroup && <CometChatMessageComposer group={selectedGroup} />}
</>
) : (
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#999" }}>
Select a conversation to start chatting
</div>
)}
</div>
</div>
);
}
```
This page is accessible at `/messages`. No router configuration needed -- Next.js handles it via the file system.
### Add a navigation link
Find the layout's nav component and add a link:
```tsx
import Link from "next/link";
// In the nav, alongside existing links:
<Link href="/messages">Messages</Link>
```
**Important:** Use Next.js's `<Link>` component (from `next/link`), not a plain `<a>` tag or React Router's `<Link>`. Next.js's Link handles client-side navigation and prefetching.
---
## 6. Route placement (Pages Router)
### Create the chat page
```tsx
// pages/messages.tsx
import dynamic from "next/dynamic";
const ChatView = dynamic(() => import("../components/ChatView"), {
ssr: false,
loading: () => (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100vh" }}>
Loading chat...
</div>
),
});
export default function MessagesPage() {
return <ChatView />;
}
```
The `ChatView` component contains the actual CometChat composition (see `cometchat-placement` for patterns). It is dynamically imported with `ssr: false` to prevent server rendering.
### ChatView implementation
```tsx
// components/ChatView.tsx
import { useState } from "react";
import {
CometChatConversations,
CometChatMessageHeader,
CometChatMessageList,
CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";
export default function ChatView() {
const [selectedUser, setSelectedUser] = useState<CometChat.User>();
const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();
function handleConversationClick(conversation: CometChat.Conversation) {
const entity = conversation.getConversationWith();
if (entity instanceof CometChat.User) {
setSelectedUser(entity);
setSelectedGroup(undefined);
} else if (entity instanceof CometChat.Group) {
setSelectedUser(undefined);
setSelectedGroup(entity);
}
}
return (
<div style={{ display: "flex", height: "100vh" }}>
<div style={{ width: "360px", borderRight: "1px solid #eee" }}>
<CometChatConversations onItemClick={handleConversationClick} />
</div>
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
{selectedUser && (
<>
<CometChatMessageHeader user={selectedUser} />
<CometChatMessageList user={selectedUser} hideReplyInThreadOption={true} />
<CometChatMessageComposer user={selectedUser} />
</>
)}
{selectedGroup && (
<>
<CometChatMessageHeader group={selectedGroup} />
<CometChatMessageList group={selectedGroup} hideReplyInThreadOption={true} />
<CometChatMessageComposer group={selectedGroup} />
</>
)}
</div>
</div>
);
}
```
---
## 7. Modal/drawer placement
### App Router
Create a Client Component for the drawer:
```tsx
// components/ChatDrawer.tsx
"use client";
import { useEffect, useState } from "react";
import {
CometChatMessageHeader,
CometChatMessageList,
CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";
interface ChatDrawerProps {
isOpen: boolean;
onClose: () => void;
targetUserId?: string;
}
export function ChatDrawer({ isOpen, onClose, targetUserId }: ChatDrawerProps) {
const [user, setUser] = useState<CometChat.User>();
useEffect(() => {
if (!isOpen || !targetUserId) return;
CometChat.getUser(targetUserId).then(setUser);
}, [isOpen, targetUserId]);
if (!isOpen) return null;
return (
<>
<div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 999, backgroundColor: "rgba(0,0,0,0.3)" }} />
<div style={{
position: "fixed", top: 0, right: 0, bottom: 0, width: "400px", zIndex: 1000,
backgroundColor: "#fff", boxShadow: "-4px 0 20px rgba(0,0,0,0.15)",
display: "flex", flexDirection: "column",
}}>
<div style={{ display: "flex", justifyContent: "space-between", padding: "12px", borderBottom: "1px solid #eee" }}>
<span style={{ fontWeight: 600 }}>Chat</span>
<button onClick={onClose} style={{ background: "none", border: "none", cursor: "pointer" }}>X</button>
</div>
{user && (
<>
<CometChatMessageHeader user={user} />
<div style={{ flex: 1, overflow: "hidden" }}>
<CometChatMessageList user={user} hideReplyInThreadOption={true} />
</div>
<CometChatMessageComposer user={user} />
</>
)}
</div>
</>
);
}
```
**Mounting the drawer:** The drawer component has `"use client"`, so it can be imported from either Server or Client Components. Import it in the layout or any page:
```tsx
// In a Server Component layout -- this works because ChatDrawer is "use client"
import { ChatDrawer } from "../components/ChatDrawer";
// But state management (isOpen) must be in a Client Component.
// Option 1: wrap in a small client component
// Option 2: use a client-side context for drawer state
```
For state lifting across the Server/Client boundary, create a small Client Component wrapper:
```tsx
// components/ChatDrawerTrigger.tsx
"use client";
import { useState } from "react";
import { ChatDrawer } from "./ChatDrawer";
export function ChatDrawerTrigger({ targetUserId }: { targetUserId: string }) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Message</button>
<ChatDrawer isOpen={isOpen} onClose={() => setIsOpen(false)} targetUserId={targetUserId} />
</>
);
}
```
### Pages Router
Use `dynamic` import for the drawer/modal component:
```tsx
import dynamic from "next/dynamic";
const ChatDrawer = dynamic(() => import("../components/ChatDrawer").then(m => m.ChatDrawer), {
ssr: false,
});
```
See `cometchat-placement` for complete modal and drawer implementations.
---
## 8. API route for production auth
Next.js can serve as both frontend and backend. Use an API route to generate CometChat auth tokens server-side.
### App Router API route
```tsx
// app/api/cometchat-token/route.ts
import { NextRequest, NextResponse } from "next/server";
const COMETCHAT_APP_ID = process.env.COMETCHAT_APP_ID!; // server-only, no NEXT_PUBLIC_ prefix
const COMETCHAT_REGION = process.env.COMETCHAT_REGION!; // server-only
const COMETCHAT_AUTH_TOKEN = process.env.COMETCHAT_AUTH_TOKEN!; // server-only secret
export async function POST(request: NextRequest) {
try {
const { uid } = await request.json();
if (!uid || typeof uid !== "string") {
return NextResponse.json({ error: "uid is required" }, { status: 400 });
}
const response = await fetch(
`https://${COMETCHAT_APP_ID}.api-${COMETCHAT_REGION}.cometchat.io/v3/users/${uid}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
apiKey: COMETCHAT_AUTH_TOKEN,
appId: COMETCHAT_APP_ID,
},
body: JSON.stringify({}),
}
);
if (!response.ok) {
const error = await response.text();
return NextResponse.json({ error }, { status: response.status });
}
const data = await response.json();
return NextResponse.json({ token: data.data.authToken });
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
```
### Pages Router API route
```tsx
// pages/api/cometchat-token.ts
import type { NextApiRequest, NextApiResponse } from "next";
const COMETCHAT_APP_ID = process.env.COMETCHAT_APP_ID!;
const COMETCHAT_REGION = process.env.COMETCHAT_REGION!;
const COMETCHAT_AUTH_TOKEN = process.env.COMETCHAT_AUTH_TOKEN!;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { uid } = req.body;
if (!uid || typeof uid !== "string") {
return res.status(400).json({ error: "uid is required" });
}
const response = await fetch(
`https://${COMETCHAT_APP_ID}.api-${COMETCHAT_REGION}.cometchat.io/v3/users/${uid}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
apiKey: COMETCHAT_AUTH_TOKEN,
appId: COMETCHAT_APP_ID,
},
body: JSON.stringify({}),
}
);
if (!response.ok) {
const error = await response.text();
return res.status(response.status).json({ error });
}
const data = await response.json();
return res.status(200).json({ token: data.data.authToken });
} catch (error) {
return res.status(500).json({ error: String(error) });
}
}
```
### Environment variables for the API route
Add server-only variables to `.env.local` (no `NEXT_PUBLIC_` prefix -- these must NOT be exposed to the browser):
```env
# .env.local -- server-only (no NEXT_PUBLIC_ prefix)
COMETCHAT_APP_ID=your_app_id
COMETCHAT_REGION=us
COMETCHAT_AUTH_TOKEN=your_server_auth_token
# Client-side (NEXT_PUBLIC_ prefix)
NEXT_PUBLIC_COMETCHAT_APP_ID=your_app_id
NEXT_PUBLIC_COMETCHAT_REGION=us
NEXT_PUBLIC_COMETCHAT_AUTH_KEY=your_client_auth_key
```
**Note:** `COMETCHAT_AUTH_TOKEN` (server secret) and `COMETCHAT_AUTH_KEY` (client key) are different values from the CometChat dashboard. The auth token has higher privileges. Never prefix it with `NEXT_PUBLIC_`.
---
## 9. Environment variables
### Next.js env var conventions
| Variable | Prefix | Accessible from | File |
|---|---|---|---|
| Client-side vars | `NEXT_PUBLIC_` | Browser + Server | `.env.local` |
| Server-only vars | None | Server only (API routes, Server Components) | `.env.local` |
### .env.local file
```env
NEXT_PUBLIC_COMETCHAT_APP_ID=your_app_id
NEXT_PUBLIC_COMETCHAT_REGION=us
NEXT_PUBLIC_COMETCHAT_AUTH_KEY=your_auth_key
```
**Access in client code:** `process.env.NEXT_PUBLIC_COMETCHAT_APP_ID`
**Important:** `.env.local` is gitignored by default in Next.js. Unlike Vite, you do not need to manually add it to `.gitignore`.
---
## 10. CSS import
### App Router
Import in `app/globals.css` or `app/layout.tsx`:
```css
/* app/globals.css */
@import "@cometchat/chat-uikit-react/css-variables.css";
/* your styles below */
```
Or as a JS import in the root layout:
```tsx
// app/layout.tsx
import "@cometchat/chat-uikit-react/css-variables.css";
import "./globals.css";
```
### Pages Router
Import in `pages/_app.tsx` or `styles/globals.css`:
```tsx
// pages/_app.tsx
import "@cometchat/chat-uikit-react/css-variables.css";
import "../styles/globals.css";
```
---
## 11. Common pitfalls
### Missing "use client"
**Symptom:** `ReferenceError: window is not defined` or `ReferenceError: document is not defined` during build or at runtime.
**Cause:** A file imports from `@cometchat/chat-uikit-react` without `"use client"` at the top. Next.js tries to render it on the server.
**Fix:** Add `"use client"` as the first line of the file.
### Server/client code mixing
**Symptom:** Build errors about `next/headers`, `cookies()`, or `generateMetadata` in the same file as CometChat imports.
**Cause:** Server-only APIs and client-only APIs cannot coexist in the same file. CometChat requires `"use client"`, but `next/headers` and `cookies()` are server-only.
**Fix:** Split the file. Keep server logic in a Server Component; keep CometChat in a separate `"use client"` component that the Server Component imports.
```tsx
// app/messages/page.tsx (Server Component -- does data fetching)
import { cookies } from "next/headers";
import dynamic from "next/dynamic";
const ChatView = dynamic(() => import("../../components/ChatView"), { ssr: false });
export default async function MessagesPage() {
const session = cookies().get("session"); // server-only
if (!session) redirect("/login");
return <ChatView />;
}
```
### Image optimization
CometChat renders avatars and media images via its own components. These do not conflict with `next/image`. Do not try to replace CometChat's internal images with `next/image` -- they are managed by the SDK.
### Middleware
CometChat has no middleware requirements. Do not add CometChat-related middleware. If the project has auth middleware (e.g., protecting routes), just ensure the chat route is behind the same auth as the rest of the app.
### ISR/SSG pages with chat
If a page uses static generation (`generateStaticParams`) or ISR, you can still add chat. Wrap the CometChat components in a Client Component. The page's static HTML ships without chat, and the Client Component hydrates and renders chat in the browser:
```tsx
// app/products/[id]/page.tsx
export async function generateStaticParams() {
// ... returns product IDs for static generation
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* ChatPanel is "use client" -- renders only in the browser */}
<ChatPanel targetUserId={product.sellerId} />
</div>
);
}
```
### Turbopack
Next.js's Turbopack (dev mode with `next dev --turbo`) works with CometChat. No special configuration needed. If you encounter issues, fall back to the standard Webpack dev server (`next dev` without `--turbo`).
### Pages Router: do NOT use CometChat from `getServerSideProps` / `getStaticProps` / `getInitialProps`
**Symptom:** `ReferenceError: window is not defined` or cryptic SDK errors during `next build` or page rendering.
**Cause:** The CometChat client SDK needs the browser (localStorage, WebSocket, etc.) and a logged-in user session. It cannot run inside `getServerSideProps`, `getStaticProps`, or `getInitialProps` — those execute on the Node.js server before the browser has hydrated.
**Fix:** Do chat-related work in client components only. If you need to pre-seed a conversation from server data, pass just the primitive IDs (user UID, group GUID) as page props and let the client component fetch the CometChat entity on mount:
```tsx
// pages/products/[id].tsx — server-side data fetch, no CometChat
export async function getServerSideProps({ params }) {
const product = await fetchProduct(params.id);
return { props: { product } }; // pass sellerId as string, not a CometChat.User
}
export default function ProductPage({ product }) {
return (
<>
<h1>{product.name}</h1>
<ChatWithSeller sellerUid={product.sellerId} /> {/* client component */}
</>
);
}
```
### Pages Router: `_document.tsx` doesn't need CometChat changes
If the project has a custom `pages/_document.tsx` for font preloading or third-party SSR CSS injection, leave it alone. CometChat CSS variables are client-side only — they don't need `_document.tsx` integration. Mount the CSS import in `_app.tsx` as shown in section 10; `_document.tsx` is the wrong layer.
---
## 12. Complete integration checklist (App Router)
1. 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.
2. Create `.env.local` with `NEXT_PUBLIC_COMETCHAT_APP_ID`, `NEXT_PUBLIC_COMETCHAT_REGION`, `NEXT_PUBLIC_COMETCHAT_AUTH_KEY`
3. Import `@cometchat/chat-uikit-react/css-variables.css` in `app/globals.css`
4. Create `app/providers/CometChatProvider.tsx` with `"use client"` (section 3)
5. Mount `CometChatProvider` in `app/layout.tsx` wrapping `{children}`
6. Create `app/messages/page.tsx` with `"use client"` (section 5)
7. Add a `<Link href="/messages">Messages</Link>` to the layout's nav
8. Verify: `npm run build` should succeed without SSR errors
## 13. Complete integration checklist (Pages Router)
1. 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.
2. Create `.env.local` with `NEXT_PUBLIC_COMETCHAT_APP_ID`, `NEXT_PUBLIC_COMETCHAT_REGION`, `NEXT_PUBLIC_COMETCHAT_AUTH_KEY`
3. Import `@cometchat/chat-uikit-react/css-variables.css` in `pages/_app.tsx`
4. Create `components/CometChatProvider.tsx` (section 3 code, without `"use client"`)
5. Dynamically import `CometChatProvider` in `pages/_app.tsx` with `ssr: false` (section 4)
6. Create `pages/messages.tsx` with dynamic import (section 6)
7. Add a `<Link href="/messages">Messages</Link>` to the layout's nav
8. Verify: `npm run build` should succeed without SSR errors
## 14. Visual Builder integration (v4.3)
If the customer picks **Visually** in dispatcher Step 3.1, the Next.js recipe diverges based on App Router vs Pages Router. Skills runs `cometchat builder export --platform react --output <target>` to download the canonical `src/CometChat/` + patch settings in one step.
**Full recipe lives in `cometchat-core` §11 "Visual Builder integration".** This section is a pointer + Next.js-specific gotchas:
### App Router (recommended for Visual Builder)
- Run `cometchat builder export --platform react --output src/app/CometChat --json`.
- Create `src/app/CometChatNoSSR/CometChatNoSSR.tsx` (client component, init + login + render).
- Create `src/app/CometChatAppWrapper.tsx` with `"use client"` + `dynamic(() => import("../app/CometChatNoSSR/CometChatNoSSR"), { ssr: false })`.
- Import the wrapper in `src/app/page.tsx`.
- **Patch `src/app/CometChat/context/CometChatContext.tsx`** to use `'../../../../package.json'` (4 levels up) instead of canonical's `'../../../package.json'` (3 levels). **Finding F16** — depth differs because the directory moved into `src/app/`.
### Pages Router (NOT recommended)
**Finding F17** (2026-05-22): Next.js Pages Router enforces "global CSS imports only in `pages/_app.tsx`". The canonical `src/CometChat/` has 25+ component-level CSS imports — Pages Router rejects the build. App Router tolerates this; Pages Router does not. Recommend App Router instead.
If a customer insists on Pages Router + Visual Builder, the only workaround is to convert all 25+ canonical CSS files into CSS Modules — heavy customer-side work. Not validated in v4.3.0.
### Both routers
- Use `process.env.NEXT_PUBLIC_COMETCHAT_*` (NOT `import.meta.env.*` which is Vite-only).
- Pin `@cometchat/[email protected]` + `@cometchat/[email protected]`.
- `package.json` needs `cometChatCustomConfig` block (Finding F2).
If the customer picks **In code**, ignore this section.