Ground truth:
@cometchat/chat-uikit-react-native@5+ catalogrn-v5.json(216 public symbols) +features.rn-v5.json. Official docs: https://www.cometchat.com/docs/ui-kit/react-native/overview · scoped index…/ui-kit/react-native/llms-react-native-v5.md. Verify every symbol against the catalog; fetch signatures from docs (references/docs-map.md); never trust memory.⚠️ React Native is NOT React. Different package, different API, native views instead of DOM. A React UI Kit snippet will not compile here.
references/docs-map.mdends with the verified React → React Native name table — read it before porting anything.
Use this skill when
- "add chat to my React Native app" · "integrate CometChat in Expo" · "RN chat UI"
- any React Native CometChat task — it is the assumed base for every other
cometchat-react-native-*skill
Prerequisites & install
One package per platform. Never mix @cometchat/chat-uikit-react (web) into an RN app.
npm install @cometchat/chat-uikit-react-native@5 @cometchat/chat-sdk-react-native@4
Then the native peer deps — required, not optional. Verified against 5.4.0 + the integration pages:
npm install @react-native-clipboard/clipboard \
react-native-gesture-handler react-native-svg react-native-video react-native-localize \
react-native-safe-area-context @react-native-async-storage/async-storage@^2.1.2 dayjs
# Expo also needs: punycode
cd ios && pod install for bare RN. A missing native module fails at MOUNT, not at build — the
error names a NativeModule, not CometChat, so it reads as unrelated.
⚠️ React Native >= 0.87: RN removed InteractionManager from core and the kit (through 5.5.0)
still calls it in the composer's iOS send path, so sending a message throws on iOS — it builds,
typechecks and bundles clean. Fix and shim: cometchat-react-native-troubleshooting -> Sending a
message throws on iOS.
Setup & credentials (essentials — full detail: references/setup-credentials.md)
- Detect the project: Expo (
app.json/app.config.js) or bare RN (metro.config.js). Reuse an existing.cometchat/config.jsonor env if present — skip re-setup. - version_conflict — STOP if
@cometchat/chat-uikit-react-nativeis already inpackage.jsonat a non-v5 major; reconcile first, never mix majors (RULES.md). - Credentials — OFFER the dashboard fetch FIRST (never silently "paste it yourself" — AUDIT-039). Offer both, defaulting to fetch: (a) dashboard fetch (recommended) —
npx @cometchat/skills-cli@3 auth login, pick an EXISTING app (provision list --json; never auto-create),provision use --app-id <id> --jsonwrites.cometchat/config.json; (b) manual paste from Dashboard → Credentials (dev-only Auth Key). Then the SKILL writes the env (§4) — the CLI stops at the fetch; env-writing and codegen are the SKILL's job. Full flow:references/setup-credentials.md. - Write env: bare RN →
.env(read viareact-native-config); Expo →.env(read viaConstants.expoConfig.extraorprocess.env). Gitignore it; never echo the Auth Key. Prod → server-minted auth token, not the Auth Key. Nothing in a mobile bundle is secret (IPA/APK are readable) — seereferences/setup-credentials.md. - Authorize =
initFromSettingsthenloginboth resolve; auth error is almost always the wrong Region.
Integration ordering (BAKED — invariant)
init() must resolve before login(). login() must resolve before you render any component.
Breaking it gives a blank screen, not an error.
Use initFromSettings, not the classic init — it persists integrationSource="ai-agent"
so CometChat can attribute the integration. The classic init does not (AUDIT-084), and a machine
gate enforces this.
import { CometChatUIKit } from "@cometchat/chat-uikit-react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
const settings: CometChat.CometChatSettings = {
appId: APP_ID,
region: REGION,
credentials: { authKey: AUTH_KEY }, // dev only — production mints an auth token server-side
chatSDK: {
presenceSubscription: { type: "ALL_USERS" },
autoEstablishSocketConnection: true,
},
};
await CometChatUIKit.initFromSettings(settings);
await CometChatUIKit.login({ uid: UID }); // or ({ authToken }) in production
Note the shape: presence moves into chatSDK.presenceSubscription.type — it is not a
top-level field and not a builder call.
⚠️ Two shapes that differ from React and will not compile if ported:
- settings is a plain object — there is no
UIKitSettingsBuilderexport in RN logintakes an object:login({ uid }), notlogin(uid)
Use an existing UID from your dashboard (the sample apps seed cometchat-uid-1). Never invent one.
The provider chain (BAKED — order is load-bearing)
Outermost first. Verified against the integration + recipe pages:
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatI18nProvider>
<SafeAreaView style={{ flex: 1 }}>
<CometChatThemeProvider>
{/* screens */}
</CometChatThemeProvider>
</SafeAreaView>
</CometChatI18nProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
GestureHandlerRootView is outermost and must carry flex: 1. Omit it and swipe / long-press
silently do nothing — no error, no warning. It is the single most common "the UI renders but nothing
responds" cause on RN.
Sizing — flex, never viewport units
There is no 100dvh and no CSS. The rule: every ancestor of a kit component is flex: 1, and a
scrolling column also needs minHeight: 0. A fixed pixel height breaks when the soft keyboard opens.
The composer must remain visible with the keyboard up — verify on a device, not only a simulator.
Component / API map (BAKED closed list — from catalogs/rn-v5.json)
| Need | Component |
|---|---|
| conversation list | CometChatConversations |
| message pane | CometChatMessageHeader · CometChatMessageList · CometChatMessageComposer |
| compact composer | CometChatCompactMessageComposer |
| threads | CometChatThreadHeader |
| search | CometChatSearch |
| users / groups / members | CometChatUsers · CometChatGroups · CometChatGroupMembers |
| calls | CometChatCallButtons · CometChatIncomingCall · CometChatOutgoingCall · CometChatOngoingCall · CometChatCallLogs |
| theming / i18n | CometChatThemeProvider + useTheme() · CometChatI18nProvider |
| extension chain | ChatConfigurator + DataSourceDecorator |
Anything not in catalogs/rn-v5.json does not exist — do not emit it. Notably absent in v5:
CometChatErrorBoundary, CometChatDetails, CometChatAddMembers, CometChatBannedMembers,
CometChatTransferOwnership, and every *WithMessages composite. See references/docs-map.md
for the SDK-fallback path for each.
Callback names — RN uses Press, not Click
| Component | Wire these |
|---|---|
CometChatConversations | onItemPress · onSearchBarClicked · onError |
CometChatMessageList | onThreadRepliesPress · onError |
CometChatMessageHeader | showBackButton (default false) + onBack · onError |
CometChatSearch | onBack · onConversationClicked · onMessageClicked |
React's onItemClick / onThreadRepliesClick do not exist here.
⚠️ On
CometChatGroupMembers,onErrortakes NO argument in kits up to 5.5.0 —onError?: () => void, unlike every other list. Type the handler with a parameter there and it fails with TS2322. WriteonError={() => …}and show the failure with theErrorViewslot, which needs no error object. Fixed in the kit (uikit-react-native#1452); once that ships, the parameter is available and this note goes away.
Golden path — the production-ready core surface
The default for an unscoped "add chat". One screen per route on a stack navigator with a real back button — never two panes side by side, and never a web-style side panel.
// ChatScreen.tsx — the message pane
import {
CometChatMessageHeader, CometChatMessageList, CometChatMessageComposer,
} from "@cometchat/chat-uikit-react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { View } from "react-native";
export const ChatScreen = ({ user, group, onBack }: {
user?: CometChat.User; group?: CometChat.Group; onBack: () => void;
}) => (
<View style={{ flex: 1 }}>
{/* showBackButton defaults to FALSE on RN — without it onBack never fires */}
<CometChatMessageHeader user={user} group={group} showBackButton onBack={onBack} />
<CometChatMessageList user={user} group={group} />
<CometChatMessageComposer user={user} group={group} />
</View>
);
// ConversationsScreen.tsx — the list, wired to open the pane
import { CometChatConversations, CometChatUiKitConstants } from "@cometchat/chat-uikit-react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
export const ConversationsScreen = ({ onOpen }: {
onOpen: (user?: CometChat.User, group?: CometChat.Group) => void;
}) => (
<CometChatConversations
onItemPress={(conversation: CometChat.Conversation) => {
const isUser =
conversation.getConversationType() ===
CometChatUiKitConstants.ConversationTypeConstants.user;
isUser
? onOpen(conversation.getConversationWith() as CometChat.User, undefined)
: onOpen(undefined, conversation.getConversationWith() as CometChat.Group);
}}
/>
);
The core surface is FOUR things, not two
contracts.rn-v5.json → core-chat-surface mandates all four by default, and the reviewer's G1
gate fails an emit that ships fewer. A list ↔ message pane is half the surface:
| Capability | Component | Wire |
|---|---|---|
| conversations | CometChatConversations | onItemPress |
| messages | CometChatMessageHeader + List + Composer | showBackButton + onBack |
| threaded replies | CometChatThreadHeader | onThreadRepliesPress → push a thread screen |
| search | CometChatSearch | showSearchBar={true} + onSearchBarClicked → push a search screen |
On RN both are pushed screens, not side panels. Search is opt-out only, and must cover the global (list) case and the in-chat case scoped to the open conversation.
// message pane — thread entry point
<CometChatMessageList
user={user}
group={group}
onThreadRepliesPress={(message) => openThread(message)}
/>
// conversation list — search entry point.
// showSearchBar defaults to FALSE on RN (it is true on React), so the callback alone
// silently ships a surface with no search. Both props, always.
<CometChatConversations
onItemPress={open}
showSearchBar={true}
onSearchBarClicked={() => openSearch()}
/>
The callbacks above are only the entry points. openThread and openSearch push screens, and
those screens are where the two mandated components actually render — emit both, or the surface
has tap targets that lead nowhere:
// ThreadScreen — what openThread(message) pushes to.
// parentMessageId is typed `string` on the list and `string | number` on the composer,
// so the list needs .toString().
function ThreadScreen({ route, navigation }) {
const { parentMessage, user, group } = route.params;
return (
<View style={{ flex: 1, minHeight: 0 }}>
<CometChatThreadHeader parentMessage={parentMessage} />
<CometChatMessageList
user={user}
group={group}
parentMessageId={parentMessage.getId().toString()}
/>
<CometChatMessageComposer
user={user}
group={group}
parentMessageId={parentMessage.getId()}
/>
</View>
);
}
// SearchScreen — what openSearch() pushes to.
// Omit uid/guid for the GLOBAL case (from the conversation list); pass one to scope the
// search to the open conversation. Both cases are mandated — one screen serves both.
function SearchScreen({ route, navigation }) {
const { user, group } = route.params ?? {};
return (
<CometChatSearch
onBack={() => navigation.goBack()}
uid={user?.getUid()}
guid={group?.getGuid()}
onConversationClicked={(conversation) => navigation.navigate("Messages", { conversation })}
onMessageClicked={(message) => navigation.navigate("Messages", { message })}
onError={(e) => console.error(e)}
/>
);
}
Search:
CometChatSearchis documented at{DOCS_BASE}/ui-kit/react-native/search(fetch it viareferences/docs-map.md); wireonConversationClicked/onMessageClickedand itsonBack. (Formerly tracked as gap RN-G1 — the page has since been published.)
Every affordance that is visible by default is wired or hidden. A tap target that does nothing is
a defect, and so is an opened surface with no way back — if you open search or a thread, wire its
onBack in the same edit.
Deep references (load only when the task needs them)
references/docs-map.md— intent → the exact docs page; the SDK-fallback table; the React→RN name table. Read before porting any React snippet.references/component-props.md— baked props + PascalCase view slots per drop-in, and the defaults that differ from React (showSearchBarisfalsehere). Read before placing custom UI — it goes in a slot, never as a sibling.references/anti-patterns.md— the 13 failures that actually break an RN integration, each with why its error names the wrong thing.references/lifecycle.md—initFromSettings→ login → render, and the promise-cached boot guard (a boolean flag does not work).references/layout.md— the four sizing invariants thecore-chat-surfacecontract gates on.references/dependencies.md— the exact peer set + the native config that is not optional (Podfile modular headers, gesture-handler import order).references/setup-credentials.md— dev vs production auth, and why nothing in a mobile bundle is secret.
Common pitfalls
- Missing
GestureHandlerRootView— gestures silently dead. Outermost,flex: 1. - Rendering before
login()resolves — blank screen, no error. - Porting React code —
UIKitSettingsBuilder,login(uid),onItemClick, CSS,document.*: none exist here. - Fixed heights — breaks on keyboard open. Use
flex: 1+minHeight: 0. - Shipping the Auth Key — dev only; production mints an auth token server-side.
Verify it works
- Every emitted symbol appears in
catalogs/rn-v5.json. - Code compiles against the pinned kit:
npm run verify:fences:rn-v5. - Oracles agree with the kit and docs:
npm run verify:sync:rn-v5. tsc --noEmitin the APP is not clean, and that is not the app's bug. The kit ships rawsrc/*.tsxas itsmain/modulewith no built declarations, so TypeScript follows the import and typechecks the kit itself (NodeJSnamespace, implicitany,isolatedModulesre-exports,User | null, and on RN 0.87 aRefObject<TextInputType>mismatch). Gate on the Metro bundle instead —npx react-native bundle --platform ios|android --dev false --entry-file index.js --bundle-output /dev/null— and if you want a typecheck, filter the kit's own files out of the output. Never "fix" node_modules, and never report the app as failing typecheck because of them.- Getting the app to actually RUN on a shared machine. React Native asks for Metro on port 8081
and takes whatever answers: another project's packager serves a foreign bundle, and the app dies
with
Invariant Violation: TurboModuleRegistry.getEnforcing('PlatformConstants')— a version mismatch that reads like a missing native module. iOS: point the app at your packager withxcrun simctl spawn <udid> defaults write <bundle-id> RCT_jsLocation "localhost:<port>"(RCT_METRO_PORTdoes not change the runtime URL). Android:adb reverse tcp:8081 tcp:<port>, and when that silently does nothing — it does on some emulators — embed the bundle instead:npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res, thenrun-android. Remove that file before going back to Fast Refresh. - On a device: chat fills the screen, the composer survives the keyboard, back works, gestures respond.
Explain what you built (REQUIRED close)
Tell the user briefly: (1) what I wired (3–5 bullets, NAME the files); (2) decisions & why, flagging dev-only AS dev-only (the Auth Key is dev-only → server-minted auth token for production; the <uid> you used; Expo vs bare RN path); (3) what I did NOT touch (additive — navigation/auth/styles intact). Then offer these THREE options as a selectable choice and WAIT — do not auto-continue:
- ① Add another feature — check what's already wired; ASK which Dashboard extensions are enabled; suggest 3–4 gaps (calls · push · AI smart replies · polls · stickers · translation · notification feed). Never suggest reactions or mentions — they are ON by default in v5. →
cometchat-react-native-features/cometchat-react-native-calls/cometchat-react-native-push. - ② Customize theming — ask for brand color / light-dark →
cometchat-react-native-customization. - ③ Test it manually — hand it back so they run it on a device.