mit einem Klick
cometchat-native-expo-patterns
// Integration patterns for Expo managed workflow — app.json config, permissions, gesture handler setup, env vars, Expo Router file-based routing subsection.
// Integration patterns for Expo managed workflow — app.json config, permissions, gesture handler setup, env vars, Expo Router file-based routing subsection.
Integration patterns for bare React Native CLI projects — pod install, Info.plist + AndroidManifest permissions, Apple privacy manifest, native module linking, Metro config.
Component catalog for the CometChat React Native UI Kit v5 — names, props, slot views, request builders, hide flags, style shape. Always loaded before writing CometChat* JSX.
Shared rules for CometChat React Native UI Kit v5. Always loaded alongside framework (expo/bare) and placement skills. Read this first.
Customize the CometChat React Native UI Kit without forking — four-tier model: props → request builders → text formatters + message templates → DataSource decorators + event bus.
Feature catalog for React Native — calls (separate SDK + WebRTC), extensions (polls / stickers / translation / link preview / collaborative doc / whiteboard / smart replies), AI agent, in-call chat. When to toggle, install, or swap.
Where to put chat in a React Native app — Stack screen, BottomTab, Modal, BottomSheet, Embedded. Maps each to CometChat component composition with ASCII layout references.
| name | cometchat-native-expo-patterns |
| description | Integration patterns for Expo managed workflow — app.json config, permissions, gesture handler setup, env vars, Expo Router file-based routing subsection. |
| license | MIT |
| compatibility | Node.js >=18; Expo SDK >=50; @cometchat/chat-uikit-react-native ^5 |
| allowed-tools | executeBash, readFile, fileSearch, listDirectory, AskUserQuestion |
| metadata | {"author":"CometChat","version":"3.0.0","tags":"cometchat react-native expo managed expo-router"} |
Teaches Claude how to integrate CometChat into an Expo managed workflow project. Covers:
app.json permissions for iOS + AndroidApp.tsx with all four wrappersexpo-constants or .envRead cometchat-native-core first (init/login/wrapper chain + anti-patterns), then cometchat-native-components (prop reference), then cometchat-native-placement (where chat goes).
Ground truth: docs/ui-kit/react-native/expo-integration.mdx, expo-conversation.mdx, expo-one-to-one-chat.mdx, expo-tab-based-chat.mdx, and examples/SampleAppExpo/.
expo in package.json dependenciesapp.json / app.config.js exists at the rootpackage.json main field references expo (e.g. "main": "index.js" with an Expo-style entry)Do NOT use this skill when:
ios/ + android/ folder at the root (that's bare RN → use cometchat-native-bare-patterns)The CometChat UI Kit depends on native modules that can't be shimmed. This means:
eas build --profile development or expo run:ios / expo run:android)The first build can take 5-15 minutes. Subsequent runs are fast via the dev client.
Before integrating, confirm the user has either:
eas-cli installed and an EAS account, ORIf neither, stop and ask them to set one up. Don't waste their time installing packages that won't run.
The UI Kit has a long peer-dep tail. Install them all in one shot so Expo's resolver doesn't miss a native module during prebuild:
# Core SDK + UI Kit
npm install @cometchat/chat-sdk-react-native
npm install @cometchat/chat-uikit-react-native
# Required peer deps (all natively-linked)
npx expo install \
@react-native-async-storage/async-storage \
@react-native-clipboard/clipboard \
@react-native-community/datetimepicker \
react-native-gesture-handler \
react-native-localize \
react-native-safe-area-context \
react-native-svg \
react-native-video
# dayjs + punycode — no native code but required by the kit
npm install dayjs punycode
Why npx expo install for the natively-linked deps? expo install picks versions compatible with the project's Expo SDK. Using npm install directly can land incompatible versions that break prebuild.
If the user's flow includes voice / video calls (the cometchat-native-features skill's § Calls gates this):
npm install @cometchat/calls-sdk-react-native
npx expo install \
@react-native-community/netinfo \
react-native-background-timer \
react-native-callstats \
react-native-webrtc
Skip these until the user actually wants calls. Adding WebRTC to an Expo project bloats the prebuild and requires extra permissions — don't speculatively enable it.
Add iOS + Android permissions so the kit's attachments, camera, mic, and media features work. Merge into existing expo.ios.infoPlist / expo.android.permissions — do not replace anything the user already has.
{
"expo": {
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Allow camera access to send photos and make video calls",
"NSMicrophoneUsageDescription": "Allow microphone access to send voice messages and make calls",
"NSPhotoLibraryUsageDescription": "Allow photo library access to send photos",
"NSPhotoLibraryAddUsageDescription": "Allow saving photos from chat to your library"
}
},
"android": {
"permissions": [
"android.permission.INTERNET",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO",
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.VIBRATE",
"android.permission.READ_MEDIA_IMAGES",
"android.permission.READ_MEDIA_VIDEO",
"android.permission.READ_MEDIA_AUDIO"
]
}
}
}
Permission-string best practice: the iOS Usage strings show in the system prompt when iOS asks the user for permission — write them as user-facing copy, not developer notes. "Camera access for video calls" is fine; "for media upload" isn't a real reason a user would accept.
app.config.js or app.config.tsMerge the same fields into the exported config. Don't switch the project from JS to JSON unless the user asks — respect their setup.
Expo projects use the same four-wrapper chain as bare RN (see cometchat-native-core § 3). The difference is the entry file — Expo uses App.tsx (or index.ts + registerRootComponent) rather than bare's index.js + AppRegistry.
// App.tsx
import "react-native-gesture-handler"; // MUST be the first import
import React from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
import { CometChatProvider } from "./src/providers/CometChatProvider";
import { AppNavigator } from "./src/navigation/AppNavigator";
import Constants from "expo-constants";
const extra = Constants.expoConfig?.extra ?? {};
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatThemeProvider>
<CometChatProvider
appId={extra.COMETCHAT_APP_ID}
region={extra.COMETCHAT_REGION}
authKey={extra.COMETCHAT_AUTH_KEY}
uid="cometchat-uid-1" // dev mode only
>
<AppNavigator />
</CometChatProvider>
</CometChatThemeProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
The CometChatProvider itself is defined per cometchat-native-core § 6 — reuse that implementation; don't invent another one.
import "react-native-gesture-handler" must be firstEven before React. Expo's entry-file hot reload otherwise loses the gesture handler patch and the composer / bottom-sheet gestures silently disable.
// At the top of App.tsx:
import "react-native-gesture-handler";
// THEN everything else
import React from "react";
Two options; pick one based on what the project already uses.
app.json extra + expo-constants (simple, recommended){
"expo": {
"extra": {
"COMETCHAT_APP_ID": "YOUR_APP_ID",
"COMETCHAT_REGION": "us",
"COMETCHAT_AUTH_KEY": "YOUR_AUTH_KEY"
}
}
}
Read via expo-constants:
import Constants from "expo-constants";
const { COMETCHAT_APP_ID, COMETCHAT_REGION, COMETCHAT_AUTH_KEY } = Constants.expoConfig?.extra ?? {};
Warning: these values end up in the client bundle. Never put REST_API_KEY or any server-side secret in expo.extra — use a backend (see cometchat-native-production).
.env + expo-dotenv / SDK-native .env supportExpo SDK 50+ supports .env out of the box via EXPO_PUBLIC_* prefix:
# .env
EXPO_PUBLIC_COMETCHAT_APP_ID=your_app_id
EXPO_PUBLIC_COMETCHAT_REGION=us
EXPO_PUBLIC_COMETCHAT_AUTH_KEY=your_auth_key
Read directly via process.env.EXPO_PUBLIC_COMETCHAT_APP_ID anywhere in your app. Any variable WITHOUT the EXPO_PUBLIC_ prefix is ONLY available in app.config.js / server scripts, not bundled — useful for REST API keys in backend-only code.
.env — Option B.Expo Router is a file-based alternative to @react-navigation/*. If the project has app/ instead of (or alongside) src/screens/, they're using Expo Router.
Detect Expo Router by checking package.json for expo-router in dependencies, and app.json for the "expo-router" plugin.
app/_layout.tsx)In Expo Router, the app/_layout.tsx file is the root layout — wrap the provider chain here instead of in App.tsx.
// app/_layout.tsx
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { CometChatThemeProvider } from "@cometchat/chat-uikit-react-native";
import { CometChatProvider } from "../src/providers/CometChatProvider";
import { Slot } from "expo-router";
import Constants from "expo-constants";
const extra = Constants.expoConfig?.extra ?? {};
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<CometChatThemeProvider>
<CometChatProvider
appId={extra.COMETCHAT_APP_ID}
region={extra.COMETCHAT_REGION}
authKey={extra.COMETCHAT_AUTH_KEY}
uid="cometchat-uid-1"
>
<Slot /> {/* Expo Router renders child routes here */}
</CometChatProvider>
</CometChatThemeProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
app/
_layout.tsx ← provider chain (above)
index.tsx ← home (could be the conversations list)
messages/
[uid].tsx ← dynamic route, one chat per uid
// app/index.tsx
import { CometChatConversations, CometChatUiKitConstants } from "@cometchat/chat-uikit-react-native";
import { router } from "expo-router";
export default function Home() {
return (
<CometChatConversations
onItemPress={(conversation) => {
const entity = conversation.getConversationWith();
const type = conversation.getConversationType();
if (type === CometChatUiKitConstants.ConversationTypeConstants.user) {
router.push(`/messages/${(entity as any).getUid()}`);
} else {
router.push(`/messages/group-${(entity as any).getGuid()}`);
}
}}
/>
);
}
// app/messages/[uid].tsx
import { useLocalSearchParams, router } from "expo-router";
import { useEffect, useState } from "react";
import { View } from "react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
import {
CometChatMessageHeader,
CometChatMessageList,
CometChatMessageComposer,
} from "@cometchat/chat-uikit-react-native";
export default function ChatScreen() {
const { uid } = useLocalSearchParams<{ uid: string }>();
const [user, setUser] = useState<CometChat.User | null>(null);
useEffect(() => {
if (!uid) return;
// Simple UID routing; group routing encodes differently in the index example above
CometChat.getUser(uid).then(setUser).catch(() => setUser(null));
}, [uid]);
if (!user) return null;
return (
<View style={{ flex: 1 }}>
<CometChatMessageHeader user={user} onBack={() => router.back()} showBackButton />
<CometChatMessageList user={user} hideReplyInThreadOption />
<CometChatMessageComposer user={user} />
</View>
);
}
app/
_layout.tsx
(tabs)/
_layout.tsx ← Tabs layout
chats.tsx
users.tsx
groups.tsx
calls.tsx
messages/
[uid].tsx
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
export default function TabsLayout() {
return (
<Tabs screenOptions={{ headerShown: false }}>
<Tabs.Screen name="chats" options={{ title: "Chats" }} />
<Tabs.Screen name="users" options={{ title: "Users" }} />
<Tabs.Screen name="groups" options={{ title: "Groups" }} />
<Tabs.Screen name="calls" options={{ title: "Calls" }} />
</Tabs>
);
}
Each tab file renders a single list component (CometChatConversations, CometChatUsers, etc. — see cometchat-native-placement § 2 Bottom tab for the component choices) and pushes to /messages/[uid] on press.
unstable_settings in _layout.tsx can break deep linking if set incorrectly. Leave it alone unless you know you need it.router.push("/messages/abc") works from a tab. router.back() returns to the tab. No extra configuration needed.useLocalSearchParams() (not useSearchParams() — that's web-only).Before the first run, Expo needs to generate native projects:
npx expo prebuild
Then run on the platform:
# iOS (requires Xcode)
npx expo run:ios
# Android (requires Android Studio)
npx expo run:android
# Or EAS for cloud builds
eas build --profile development --platform ios
Subsequent runs use npx expo start with the dev client — no rebuild needed unless native deps change.
When to rebuild vs. reload:
r to reload or save in Fast Refreshnpx expo prebuild --clean && npx expo run:iosapp.json permissions or plugins → npx expo prebuild --cleannpx tsc --noEmit # TypeScript check
Then in the running app:
If any of these don't work, see cometchat-native-troubleshooting.
import "react-native-gesture-handler" is the first line of the entry file (App.tsx or app/_layout.tsx for Expo Router). Not second. Not after any React import.npx expo install, not npm install, for native modules. Expo's resolver picks SDK-compatible versions.REST_API_KEY (or any server-side secret) to app.json extra — it ends up in the client bundle. Use a server endpoint (see cometchat-native-production).app.json, don't replace. The user may already have permissions for other libraries; wipe them out and their other features break.npx expo prebuild --clean after changing app.json permissions or adding native deps. Without it, iOS + Android see the old config.<CometChatMessageList> must include hideReplyInThreadOption unless you're also wiring a full thread panel (see cometchat-native-placement § Hard rule 5).cometchat-native-core § 3). For Expo Router, that's app/_layout.tsx. For plain Expo, that's App.tsx.Q: Can I use npx create-expo-app --template tabs?
Yes — the tabs template already has Expo Router set up. Just add the provider chain in app/_layout.tsx per § 5a and replace a tab's content with a CometChat component.
Q: Can I use SDK ≤49?
Expo SDK 49 may work but the CometChat peer deps target 50+ conventions. If the user is stuck on an older SDK, ask them to upgrade — or fall back to bare RN via npx expo prebuild + cometchat-native-bare-patterns.
Q: I'm seeing "Main module field cannot be resolved" when opening Expo Go.
That's the "Expo Go doesn't support native modules" error. Build a dev client: eas build --profile development or npx expo run:ios.
Q: My app crashes on first push-notification receive.
Push notifications need additional setup (APNs + FCM + maybe expo-notifications). Out of scope for this skill — see cometchat-native-troubleshooting § Push notifications.
| Skill | When to route |
|---|---|
cometchat-native-core | Init / login / wrapper chain / anti-patterns |
cometchat-native-components | Component prop reference |
cometchat-native-placement | Where chat goes (stack / tabs / modal / bottom sheet / embedded) |
cometchat-native-expo-patterns | This skill — Expo managed workflow specifics |
cometchat-native-bare-patterns | Bare RN (pod install, native modules, privacy manifest) |
cometchat-native-features | Calls, extensions, AI |
cometchat-native-theming | Theme customization |
cometchat-native-customization | Text formatters, events, custom views |
cometchat-native-production | Server-side auth tokens + user management |
cometchat-native-troubleshooting | Prebuild failures, Expo Go errors, keyboard issues, blank chat |