@cometchat/skills 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,687 @@
1
+ ---
2
+ name: cometchat-astro-patterns
3
+ description: "Framework-specific patterns for integrating CometChat React UI Kit v6 into Astro projects using React islands. Covers client:only rendering, island communication, CSS handling, and common pitfalls."
4
+ license: "MIT"
5
+ compatibility: "Node.js >=18; React >=18; Astro >=3; @astrojs/react; @cometchat/chat-uikit-react ^6; @cometchat/chat-sdk-javascript ^4"
6
+ allowed-tools: "executeBash, readFile, fileSearch, listDirectory"
7
+ metadata:
8
+ author: "CometChat"
9
+ version: "3.0.0"
10
+ tags: "chat cometchat astro react islands client-only patterns"
11
+ ---
12
+
13
+ ## Purpose
14
+
15
+ This skill teaches Claude how to integrate CometChat into an Astro project using React islands. Astro is a static-first framework -- most of the page is HTML rendered at build time. Interactive React components run as isolated "islands" in the browser. CometChat components are React islands that must use `client:only="react"` to bypass server rendering entirely.
16
+
17
+ **Read these companion skills first:**
18
+ - `cometchat-core` -- initialization, login, CSS, provider pattern, anti-patterns
19
+ - `cometchat-components` -- component catalog and composition patterns
20
+ - `cometchat-placement` -- WHERE to put chat (route, modal, drawer, embedded)
21
+
22
+ ---
23
+
24
+ ## 1. Project detection
25
+
26
+ A project uses Astro when `package.json` has `astro` as a dependency. CometChat integration also requires the React integration:
27
+
28
+ ```bash
29
+ # Check for Astro + React
30
+ grep -E '"astro"|"@astrojs/react"' package.json
31
+ ```
32
+
33
+ **If `@astrojs/react` is missing**, it must be installed first:
34
+
35
+ ```bash
36
+ npx astro add react
37
+ ```
38
+
39
+ This adds `@astrojs/react` to `package.json` and configures it in `astro.config.mjs`.
40
+
41
+ Verify the React integration is configured:
42
+
43
+ ```bash
44
+ # astro.config.mjs should have react() in the integrations array
45
+ grep -A 5 "integrations" astro.config.mjs 2>/dev/null || grep -A 5 "integrations" astro.config.ts 2>/dev/null
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 2. Critical: client:only="react"
51
+
52
+ Every Astro component that renders CometChat MUST use `client:only="react"`. This is the single most important rule for Astro + CometChat.
53
+
54
+ ### Why client:only and not client:load or client:visible
55
+
56
+ Astro's client directives control when and how interactive components are hydrated:
57
+
58
+ | Directive | Server renders? | When hydrates? | Works with CometChat? |
59
+ |---|---|---|---|
60
+ | `client:load` | Yes | On page load | **NO** -- server render crashes |
61
+ | `client:visible` | Yes | When visible in viewport | **NO** -- server render crashes |
62
+ | `client:idle` | Yes | When browser is idle | **NO** -- server render crashes |
63
+ | `client:only="react"` | No | On page load (client-only) | **YES** |
64
+
65
+ `client:load`, `client:visible`, and `client:idle` all attempt to render the component on the server first, then hydrate in the browser. CometChat components access `window` and `document` at import time, so server rendering crashes with `ReferenceError: window is not defined`.
66
+
67
+ `client:only="react"` skips server rendering entirely. The component only runs in the browser. This is the ONLY valid directive for CometChat.
68
+
69
+ ### Usage in .astro files
70
+
71
+ ```astro
72
+ ---
73
+ import ChatView from "../components/ChatView";
74
+ ---
75
+
76
+ <!-- CORRECT -->
77
+ <ChatView client:only="react" />
78
+
79
+ <!-- WRONG -- will crash during build -->
80
+ <ChatView client:load />
81
+
82
+ <!-- WRONG -- will crash during build -->
83
+ <ChatView client:visible />
84
+
85
+ <!-- WRONG -- no directive, component won't be interactive at all -->
86
+ <ChatView />
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 3. CometChatProvider for Astro
92
+
93
+ The provider pattern is the same React component as in other frameworks, but in Astro it runs entirely inside a React island. The provider is not mounted at the Astro layout level -- it is mounted inside the React island.
94
+
95
+ ### Full implementation
96
+
97
+ ```tsx
98
+ // src/components/CometChatProvider.tsx
99
+ import React, { useEffect, useState, createContext, useContext } from "react";
100
+ import { CometChatUIKit, UIKitSettingsBuilder } from "@cometchat/chat-uikit-react";
101
+ import "@cometchat/chat-uikit-react/css-variables.css";
102
+
103
+ interface CometChatContextValue {
104
+ isReady: boolean;
105
+ error: string | null;
106
+ }
107
+
108
+ const CometChatContext = createContext<CometChatContextValue>({
109
+ isReady: false,
110
+ error: null,
111
+ });
112
+
113
+ export const useCometChat = () => useContext(CometChatContext);
114
+
115
+ // Module-level state prevents both double-init AND double-login in React
116
+ // StrictMode. Without the loginInFlight guard, a second mount calls
117
+ // login() while the first is still pending and the SDK throws
118
+ // "Please wait until the previous login request ends."
119
+ let initialized = false;
120
+ let loginInFlight: Promise<unknown> | null = null;
121
+
122
+ async function ensureLoggedIn(
123
+ uid: string,
124
+ authToken?: string,
125
+ ): Promise<void> {
126
+ const existing = await CometChatUIKit.getLoggedinUser();
127
+ if (existing) return;
128
+ if (loginInFlight) {
129
+ await loginInFlight;
130
+ return;
131
+ }
132
+ loginInFlight = authToken
133
+ ? CometChatUIKit.loginWithAuthToken(authToken)
134
+ : CometChatUIKit.login(uid);
135
+ try {
136
+ await loginInFlight;
137
+ } finally {
138
+ loginInFlight = null;
139
+ }
140
+ }
141
+
142
+ interface CometChatProviderProps {
143
+ children: React.ReactNode;
144
+ }
145
+
146
+ export function CometChatProvider({ children }: CometChatProviderProps) {
147
+ const [isReady, setIsReady] = useState(false);
148
+ const [error, setError] = useState<string | null>(null);
149
+
150
+ useEffect(() => {
151
+ async function setup() {
152
+ try {
153
+ if (!initialized) {
154
+ initialized = true;
155
+
156
+ const settings = new UIKitSettingsBuilder()
157
+ .setAppId(import.meta.env.PUBLIC_COMETCHAT_APP_ID)
158
+ .setRegion(import.meta.env.PUBLIC_COMETCHAT_REGION)
159
+ .setAuthKey(import.meta.env.PUBLIC_COMETCHAT_AUTH_KEY)
160
+ .subscribePresenceForAllUsers()
161
+ .build();
162
+
163
+ await CometChatUIKit.init(settings);
164
+ }
165
+
166
+ await ensureLoggedIn("cometchat-uid-1"); // DEVELOPMENT ONLY — see cometchat-production skill
167
+
168
+ setIsReady(true);
169
+ } catch (e) {
170
+ setError(String(e));
171
+ }
172
+ }
173
+
174
+ setup();
175
+ }, []);
176
+
177
+ if (error) {
178
+ return (
179
+ <div style={{ color: "red", padding: 16, fontFamily: "monospace" }}>
180
+ CometChat Error: {error}
181
+ </div>
182
+ );
183
+ }
184
+
185
+ if (!isReady) return null;
186
+
187
+ return (
188
+ <CometChatContext.Provider value={{ isReady, error }}>
189
+ {children}
190
+ </CometChatContext.Provider>
191
+ );
192
+ }
193
+ ```
194
+
195
+ ### Key difference: CSS import is INSIDE the component
196
+
197
+ Notice that `@cometchat/chat-uikit-react/css-variables.css` is imported inside the React component file, not in an Astro layout or global stylesheet. This is because `client:only` islands are completely isolated from Astro's CSS pipeline. Stylesheets imported in `.astro` files or global CSS do NOT reach `client:only` islands.
198
+
199
+ ---
200
+
201
+ ## 4. Chat page with React island
202
+
203
+ ### Full page implementation
204
+
205
+ ```astro
206
+ ---
207
+ // src/pages/messages.astro
208
+ import Layout from "../layouts/Layout.astro";
209
+ import ChatView from "../components/ChatView";
210
+ ---
211
+
212
+ <Layout title="Messages">
213
+ <div class="chat-container">
214
+ <ChatView client:only="react" />
215
+ </div>
216
+ </Layout>
217
+
218
+ <style>
219
+ .chat-container {
220
+ height: calc(100vh - 64px); /* subtract header height */
221
+ width: 100%;
222
+ }
223
+ </style>
224
+ ```
225
+
226
+ ### ChatView component
227
+
228
+ ```tsx
229
+ // src/components/ChatView.tsx
230
+ import { useState } from "react";
231
+ import { CometChatProvider } from "./CometChatProvider";
232
+ import {
233
+ CometChatConversations,
234
+ CometChatMessageHeader,
235
+ CometChatMessageList,
236
+ CometChatMessageComposer,
237
+ } from "@cometchat/chat-uikit-react";
238
+ import { CometChat } from "@cometchat/chat-sdk-javascript";
239
+
240
+ export default function ChatView() {
241
+ return (
242
+ <CometChatProvider>
243
+ <ChatContent />
244
+ </CometChatProvider>
245
+ );
246
+ }
247
+
248
+ function ChatContent() {
249
+ const [selectedUser, setSelectedUser] = useState<CometChat.User>();
250
+ const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();
251
+
252
+ function handleConversationClick(conversation: CometChat.Conversation) {
253
+ const entity = conversation.getConversationWith();
254
+ if (entity instanceof CometChat.User) {
255
+ setSelectedUser(entity);
256
+ setSelectedGroup(undefined);
257
+ } else if (entity instanceof CometChat.Group) {
258
+ setSelectedUser(undefined);
259
+ setSelectedGroup(entity);
260
+ }
261
+ }
262
+
263
+ return (
264
+ <div style={{ display: "flex", height: "100%" }}>
265
+ <div style={{ width: "360px", borderRight: "1px solid #eee" }}>
266
+ <CometChatConversations onItemClick={handleConversationClick} />
267
+ </div>
268
+ <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
269
+ {(selectedUser || selectedGroup) ? (
270
+ <>
271
+ {selectedUser && <CometChatMessageHeader user={selectedUser} />}
272
+ {selectedGroup && <CometChatMessageHeader group={selectedGroup} />}
273
+ {selectedUser && <CometChatMessageList user={selectedUser} />}
274
+ {selectedGroup && <CometChatMessageList group={selectedGroup} />}
275
+ {selectedUser && <CometChatMessageComposer user={selectedUser} />}
276
+ {selectedGroup && <CometChatMessageComposer group={selectedGroup} />}
277
+ </>
278
+ ) : (
279
+ <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#999" }}>
280
+ Select a conversation to start chatting
281
+ </div>
282
+ )}
283
+ </div>
284
+ </div>
285
+ );
286
+ }
287
+ ```
288
+
289
+ **Important:** The `CometChatProvider` wraps the content INSIDE the React island, not at the Astro level. Each island is an independent React tree. The provider initializes CometChat when this specific island mounts.
290
+
291
+ ---
292
+
293
+ ## 5. Drawer and modal patterns
294
+
295
+ ### Chat drawer as a React island
296
+
297
+ ```tsx
298
+ // src/components/ChatDrawerIsland.tsx
299
+ import { useState, useEffect } from "react";
300
+ import { CometChatProvider } from "./CometChatProvider";
301
+ import {
302
+ CometChatMessageHeader,
303
+ CometChatMessageList,
304
+ CometChatMessageComposer,
305
+ } from "@cometchat/chat-uikit-react";
306
+ import { CometChat } from "@cometchat/chat-sdk-javascript";
307
+
308
+ interface ChatDrawerIslandProps {
309
+ targetUserId: string;
310
+ }
311
+
312
+ export default function ChatDrawerIsland({ targetUserId }: ChatDrawerIslandProps) {
313
+ const [isOpen, setIsOpen] = useState(false);
314
+
315
+ return (
316
+ <CometChatProvider>
317
+ <button onClick={() => setIsOpen(true)}>Message</button>
318
+
319
+ {isOpen && (
320
+ <>
321
+ <div
322
+ onClick={() => setIsOpen(false)}
323
+ style={{ position: "fixed", inset: 0, zIndex: 999, backgroundColor: "rgba(0,0,0,0.3)" }}
324
+ />
325
+ <div style={{
326
+ position: "fixed", top: 0, right: 0, bottom: 0, width: "400px", zIndex: 1000,
327
+ backgroundColor: "#fff", boxShadow: "-4px 0 20px rgba(0,0,0,0.15)",
328
+ display: "flex", flexDirection: "column",
329
+ }}>
330
+ <div style={{ display: "flex", justifyContent: "space-between", padding: "12px", borderBottom: "1px solid #eee" }}>
331
+ <span style={{ fontWeight: 600 }}>Chat</span>
332
+ <button onClick={() => setIsOpen(false)} style={{ background: "none", border: "none", cursor: "pointer" }} aria-label="Close">&times;</button>
333
+ </div>
334
+ <DrawerContent targetUserId={targetUserId} />
335
+ </div>
336
+ </>
337
+ )}
338
+ </CometChatProvider>
339
+ );
340
+ }
341
+
342
+ function DrawerContent({ targetUserId }: { targetUserId: string }) {
343
+ const [user, setUser] = useState<CometChat.User>();
344
+
345
+ useEffect(() => {
346
+ CometChat.getUser(targetUserId).then(setUser);
347
+ }, [targetUserId]);
348
+
349
+ if (!user) return <div style={{ padding: 16 }}>Loading...</div>;
350
+
351
+ return (
352
+ <>
353
+ <CometChatMessageHeader user={user} />
354
+ <div style={{ flex: 1, overflow: "hidden" }}>
355
+ <CometChatMessageList user={user} />
356
+ </div>
357
+ <CometChatMessageComposer user={user} />
358
+ </>
359
+ );
360
+ }
361
+ ```
362
+
363
+ Usage in an Astro page:
364
+
365
+ ```astro
366
+ ---
367
+ import Layout from "../layouts/Layout.astro";
368
+ import ChatDrawerIsland from "../components/ChatDrawerIsland";
369
+ ---
370
+
371
+ <Layout title="Product">
372
+ <h1>Product Details</h1>
373
+ <p>Some product description...</p>
374
+ <ChatDrawerIsland client:only="react" targetUserId="seller-uid-123" />
375
+ </Layout>
376
+ ```
377
+
378
+ **Note:** The trigger button is INSIDE the React island. Astro's static HTML cannot trigger React state changes directly. The button must be part of the same React tree.
379
+
380
+ ---
381
+
382
+ ## 6. Inter-island communication
383
+
384
+ Astro's island architecture means different React islands are separate React trees. They cannot share React state, context, or refs. If you need communication between a navbar island and a chat island, use one of these approaches:
385
+
386
+ ### Option A: Custom DOM events
387
+
388
+ ```tsx
389
+ // NavbarIsland.tsx -- fires a custom event
390
+ function NavbarIsland() {
391
+ function openChat() {
392
+ window.dispatchEvent(new CustomEvent("open-chat", { detail: { userId: "uid-123" } }));
393
+ }
394
+
395
+ return <button onClick={openChat}>Messages</button>;
396
+ }
397
+
398
+ // ChatIsland.tsx -- listens for the event
399
+ function ChatIsland() {
400
+ const [isOpen, setIsOpen] = useState(false);
401
+ const [targetUserId, setTargetUserId] = useState<string>();
402
+
403
+ useEffect(() => {
404
+ function handleOpenChat(e: CustomEvent) {
405
+ setTargetUserId(e.detail.userId);
406
+ setIsOpen(true);
407
+ }
408
+
409
+ window.addEventListener("open-chat", handleOpenChat as EventListener);
410
+ return () => window.removeEventListener("open-chat", handleOpenChat as EventListener);
411
+ }, []);
412
+
413
+ // ... render chat drawer when isOpen
414
+ }
415
+ ```
416
+
417
+ ### Option B: Nanostores (Astro's recommended approach)
418
+
419
+ Install nanostores: `npm install nanostores @nanostores/react`
420
+
421
+ ```typescript
422
+ // src/stores/chatStore.ts
423
+ import { atom } from "nanostores";
424
+
425
+ export const $chatOpen = atom(false);
426
+ export const $chatTargetUserId = atom<string | undefined>(undefined);
427
+ ```
428
+
429
+ ```tsx
430
+ // NavbarIsland.tsx
431
+ import { useStore } from "@nanostores/react";
432
+ import { $chatOpen, $chatTargetUserId } from "../stores/chatStore";
433
+
434
+ function NavbarIsland() {
435
+ function openChat() {
436
+ $chatTargetUserId.set("uid-123");
437
+ $chatOpen.set(true);
438
+ }
439
+
440
+ return <button onClick={openChat}>Messages</button>;
441
+ }
442
+ ```
443
+
444
+ ```tsx
445
+ // ChatIsland.tsx
446
+ import { useStore } from "@nanostores/react";
447
+ import { $chatOpen, $chatTargetUserId } from "../stores/chatStore";
448
+
449
+ function ChatIsland() {
450
+ const isOpen = useStore($chatOpen);
451
+ const targetUserId = useStore($chatTargetUserId);
452
+
453
+ // ... render chat drawer when isOpen
454
+ }
455
+ ```
456
+
457
+ Nanostores work across framework boundaries -- if the project also has Svelte or Vue islands, they can all share the same store.
458
+
459
+ ### Option C: URL-based state
460
+
461
+ Navigate to a chat page with query parameters:
462
+
463
+ ```astro
464
+ <!-- In the navbar (static HTML or any island) -->
465
+ <a href="/messages?user=uid-123">Message this user</a>
466
+ ```
467
+
468
+ ```tsx
469
+ // ChatView.tsx -- reads user from URL
470
+ function ChatView() {
471
+ const params = new URLSearchParams(window.location.search);
472
+ const targetUserId = params.get("user");
473
+
474
+ // ... resolve and render chat for this user
475
+ }
476
+ ```
477
+
478
+ ---
479
+
480
+ ## 7. Environment variables
481
+
482
+ ### Astro env var conventions
483
+
484
+ Astro uses Vite under the hood. Client-side variables must have the `PUBLIC_` prefix:
485
+
486
+ ```env
487
+ PUBLIC_COMETCHAT_APP_ID=your_app_id
488
+ PUBLIC_COMETCHAT_REGION=us
489
+ PUBLIC_COMETCHAT_AUTH_KEY=your_auth_key
490
+ ```
491
+
492
+ **Access in code:** `import.meta.env.PUBLIC_COMETCHAT_APP_ID`
493
+
494
+ Variables without `PUBLIC_` prefix are server-only (available in Astro's frontmatter and API routes, but not in client islands).
495
+
496
+ ### .env file
497
+
498
+ Create `.env` in the project root. Astro's `.env` is NOT gitignored by default -- add it:
499
+
500
+ ```bash
501
+ echo ".env" >> .gitignore
502
+ ```
503
+
504
+ ### Server-only variables (for production auth)
505
+
506
+ ```env
507
+ # Server-only (no PUBLIC_ prefix) -- for API endpoints
508
+ COMETCHAT_AUTH_TOKEN=your_server_secret
509
+ COMETCHAT_APP_ID=your_app_id
510
+ COMETCHAT_REGION=us
511
+ ```
512
+
513
+ Access in Astro API routes or server-side code:
514
+
515
+ ```typescript
516
+ // src/pages/api/cometchat-token.ts
517
+ import type { APIRoute } from "astro";
518
+
519
+ export const POST: APIRoute = async ({ request }) => {
520
+ const { uid } = await request.json();
521
+ const appId = import.meta.env.COMETCHAT_APP_ID;
522
+ const region = import.meta.env.COMETCHAT_REGION;
523
+ const authToken = import.meta.env.COMETCHAT_AUTH_TOKEN;
524
+
525
+ const response = await fetch(
526
+ `https://${appId}.api-${region}.cometchat.io/v3/users/${uid}/auth_tokens`,
527
+ {
528
+ method: "POST",
529
+ headers: {
530
+ "Content-Type": "application/json",
531
+ apiKey: authToken,
532
+ appId: appId,
533
+ },
534
+ body: JSON.stringify({}),
535
+ }
536
+ );
537
+
538
+ const data = await response.json();
539
+ return new Response(JSON.stringify({ token: data.data.authToken }), {
540
+ headers: { "Content-Type": "application/json" },
541
+ });
542
+ };
543
+ ```
544
+
545
+ **Note:** Astro API routes require on-demand rendering. In Astro 3: set `output: "server"` or `output: "hybrid"` in `astro.config.mjs`. In Astro 4+: the default is `output: "static"` with per-route opt-in — add `export const prerender = false;` at the top of the API route file. If the project is fully static, the auth endpoint must be hosted elsewhere.
546
+
547
+ ---
548
+
549
+ ## 8. CSS handling
550
+
551
+ ### The island CSS isolation problem
552
+
553
+ Unlike other frameworks, Astro's `client:only` islands are completely isolated from the Astro CSS pipeline. CSS imported in `.astro` files, global stylesheets linked in `<head>`, and `<style>` tags in Astro layouts do NOT reach `client:only` React components.
554
+
555
+ ### Solution: import CSS inside the React component
556
+
557
+ ```tsx
558
+ // src/components/ChatView.tsx
559
+ import "@cometchat/chat-uikit-react/css-variables.css"; // MUST be here, not in .astro
560
+ import { CometChatConversations } from "@cometchat/chat-uikit-react";
561
+
562
+ export default function ChatView() {
563
+ return <CometChatConversations />;
564
+ }
565
+ ```
566
+
567
+ ### Where NOT to import CSS
568
+
569
+ ```astro
570
+ ---
571
+ // src/layouts/Layout.astro
572
+ // WRONG -- this CSS won't reach client:only islands
573
+ ---
574
+ <html>
575
+ <head>
576
+ <link rel="stylesheet" href="@cometchat/chat-uikit-react/css-variables.css" />
577
+ </head>
578
+ <body><slot /></body>
579
+ </html>
580
+ ```
581
+
582
+ ```css
583
+ /* src/styles/global.css */
584
+ /* WRONG -- @import here won't reach client:only islands */
585
+ @import "@cometchat/chat-uikit-react/css-variables.css";
586
+ ```
587
+
588
+ ### Theming overrides
589
+
590
+ To override CometChat CSS variables in Astro, do it inside the React component:
591
+
592
+ ```tsx
593
+ // src/components/ChatView.tsx
594
+ import "@cometchat/chat-uikit-react/css-variables.css";
595
+ import "./cometchat-overrides.css"; // your overrides, imported AFTER
596
+
597
+ export default function ChatView() {
598
+ // ...
599
+ }
600
+ ```
601
+
602
+ ```css
603
+ /* src/components/cometchat-overrides.css */
604
+ :root {
605
+ --cometchat-primary-color: #6851d6;
606
+ --cometchat-font-family: "Inter", sans-serif;
607
+ }
608
+ ```
609
+
610
+ ---
611
+
612
+ ## 9. Common pitfalls
613
+
614
+ ### client:load vs client:only
615
+
616
+ **Symptom:** `ReferenceError: window is not defined` during `astro build` or `astro dev`.
617
+
618
+ **Cause:** Using `client:load` (or `client:visible`, `client:idle`) instead of `client:only="react"`. These directives attempt server-side rendering.
619
+
620
+ **Fix:** Replace with `client:only="react"`. Always. For every CometChat component.
621
+
622
+ ### CSS not appearing
623
+
624
+ **Symptom:** CometChat components render with no styling -- raw unstyled HTML, missing colors, broken layout.
625
+
626
+ **Cause:** CSS imported in an Astro layout or global stylesheet does not reach `client:only` islands.
627
+
628
+ **Fix:** Import `@cometchat/chat-uikit-react/css-variables.css` inside the React component file (section 8).
629
+
630
+ ### View Transitions
631
+
632
+ Astro's View Transitions API enables smooth page transitions without full reloads. CometChat's WebSocket connection persists across view transitions (the connection is on `window`, which survives transitions). However, the React island REMOUNTS on each navigation because Astro replaces the DOM.
633
+
634
+ To keep chat state across page navigations, add `transition:persist` to the island element:
635
+
636
+ ```astro
637
+ <ChatView client:only="react" transition:persist />
638
+ ```
639
+
640
+ With `transition:persist`, Astro keeps the same DOM element across navigations, so the React tree stays mounted and chat state is preserved. Without it, navigating away and back remounts the island, requiring re-initialization (the `initialized` flag prevents double-init, but the UI state resets).
641
+
642
+ ### Content Collections
643
+
644
+ Astro's Content Collections are for static content (Markdown, MDX, JSON). Chat data is dynamic and comes from CometChat's SDK. Do not try to store or query chat data via Content Collections.
645
+
646
+ ### Multiple chat islands on one page
647
+
648
+ If a page has multiple `client:only="react"` islands that use CometChat, each island is a separate React tree. The module-level `initialized` flag ensures CometChat only initializes once even with multiple islands (the flag is shared across imports of the same module).
649
+
650
+ However, each island needs its own `CometChatProvider` wrapping its content. The provider's `isReady` state is local to each React tree.
651
+
652
+ ### Passing data from Astro to React islands
653
+
654
+ Props passed to `client:only` components must be serializable (strings, numbers, booleans, plain objects, arrays). You cannot pass React components, functions, or class instances from Astro frontmatter:
655
+
656
+ ```astro
657
+ ---
658
+ // CORRECT -- serializable props
659
+ const userId = "cometchat-uid-1";
660
+ ---
661
+ <ChatDrawerIsland client:only="react" targetUserId={userId} />
662
+
663
+ ---
664
+ // WRONG -- function props are not serializable
665
+ const handleClose = () => console.log("closed");
666
+ ---
667
+ <ChatDrawerIsland client:only="react" onClose={handleClose} />
668
+ ```
669
+
670
+ ---
671
+
672
+ ## 10. Complete integration checklist
673
+
674
+ 1. Ensure `@astrojs/react` is installed: `npx astro add react`
675
+ 2. Install packages: `npm install @cometchat/chat-uikit-react @cometchat/chat-sdk-javascript`
676
+ 3. Create `.env` with `PUBLIC_COMETCHAT_APP_ID`, `PUBLIC_COMETCHAT_REGION`, `PUBLIC_COMETCHAT_AUTH_KEY`
677
+ 4. Add `.env` to `.gitignore`
678
+ 5. Create `src/components/CometChatProvider.tsx` with CSS import inside it (section 3)
679
+ 6. Create `src/components/ChatView.tsx` wrapping content in `CometChatProvider` (section 4)
680
+ 7. Create `src/pages/messages.astro` with `<ChatView client:only="react" />` (section 4)
681
+ 8. Add a "Messages" link to the Astro layout's nav
682
+ 9. Verify: `npm run build` should succeed without `window is not defined` errors
683
+
684
+ **The three things to remember for Astro:**
685
+ 1. Always `client:only="react"` -- never `client:load`
686
+ 2. CSS imports go INSIDE the React component -- never in `.astro` files
687
+ 3. Each island wraps its own `CometChatProvider` -- there is no global provider at the Astro level