Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
cometchat avatar

Cometchat Native Expo Patterns

  • 7 installs
  • 70 repo stars
  • Updated June 23, 2026
  • cometchat/cometchat-skills

Integrate CometChat into an Expo managed workflow project with app.json config, permissions, provider wiring, env vars, and Expo Router routing.

About

Teaches CometChat integration for Expo managed workflow projects, covering app.json permissions, the provider chain, and Expo Router. A developer uses it when their RN project uses Expo with app.json.

  • Configures app.json permissions for iOS and Android
  • Includes an Expo Router file-based routing subsection

Cometchat Native Expo Patterns by the numbers

  • 7 all-time installs (skills.sh)
  • Ranked #801 of 1,039 Mobile Development skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cometchat/cometchat-skills --skill cometchat-native-expo-patterns

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs7
repo stars70
Last updatedJune 23, 2026
Repositorycometchat/cometchat-skills

What it does

Integrate CometChat into an Expo managed workflow project with app.json config, permissions, provider wiring, env vars, and Expo Router routing.

Files

SKILL.mdMarkdownGitHub ↗

Purpose

Teaches Claude how to integrate CometChat into an Expo managed workflow project. Covers:

  • Installing the full peer-dependency set (not just the UI Kit)
  • Configuring app.json permissions for iOS + Android
  • Wiring the provider chain in App.tsx with all four wrappers
  • Optional calling SDK setup
  • Env vars via expo-constants or .env
  • Expo Router subsection (file-based routing)
  • Prebuild + run cadence

Read `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/.

---

Use this skill when

  • Project has expo in package.json dependencies
  • app.json / app.config.js exists at the root
  • package.json main field references expo (e.g. "main": "index.js" with an Expo-style entry)
  • The user says "Expo", "Expo Router", "managed workflow", or "EAS"

Do NOT use this skill when:

  • The project has an ios/ + android/ folder at the root (that's bare RN → use cometchat-native-bare-patterns)
  • The user says "bare React Native", "React Native CLI", or "ejected"

---

Hard prerequisite — Expo Go is NOT supported

The CometChat UI Kit depends on native modules that can't be shimmed. This means:

  • Expo Go won't load your app — you'll see "Main module field cannot be resolved" or similar
  • You must build a development client (eas build --profile development or expo run:ios / expo run:android)
  • Or install in a plain Expo simulator via prebuild

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, OR
  • Xcode + Android Studio for local prebuilds

If neither, stop and ask them to set one up. Don't waste their time installing packages that won't run.

---

Step 1 — Install dependencies

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.

Optional — calling SDK

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 \
  react-native-url-polyfill \
  react-native-performance \
  valibot
npx expo install \
  @react-native-community/netinfo \
  react-native-background-timer \
  react-native-callstats \
  react-native-webrtc
`react-native-url-polyfill`, `react-native-performance`, and `valibot` are not in the calls-sdk `peerDependencies` array — but the calls-sdk's dist/polyfills/browser.js imports them at module top, so Metro fails the bundle without them. Omitting any of the three yields Unable to resolve module … at startup with no app render. (Validated 2026-05-26 on @cometchat/calls-sdk-react-native@5.0.0 + Expo SDK 56.)

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.

---

Step 2 — Configure app.json

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.

If the project uses app.config.js or app.config.ts

Merge the same fields into the exported config. Don't switch the project from JS to JSON unless the user asks — respect their setup.

---

Step 3 — Wire the provider chain in App.tsx

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 first

Even 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";

---

Step 4 — Env vars

Two options; pick one based on what the project already uses.

Option A — 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).

⚠️ Dev-client manifest caching trap (validated 2026-05-14). Constants.expoConfig?.extra reads from the manifest the dev client baked in at `expo prebuild` time — NOT from the live app.json. Editing app.json → expo.extra and reloading the app does NOT pick up new values; the dev client keeps serving the prebuild-time snapshot. Two workarounds:

  • For dev iteration on credentials: hardcode in src/config/*.ts (Metro hot-bundles source changes immediately):
  // src/config/cometchat.ts
  export const COMETCHAT_APP_ID = "...";
  export const COMETCHAT_REGION = "us";
  export const COMETCHAT_AUTH_KEY = "...";
  • For one-shot setup or production: after every app.json → extra change, run
  npx expo prebuild --clean && npx expo run:android

to regenerate the manifest snapshot.

Silent failure mode: app reads stale App ID / Auth Key / Region → login fails with cryptic errors (User not found, region mismatch). Hit on the rn-existing cohort 2026-05-14; spent ~30 min before realizing the manifest cache was serving an old App ID.

Option B — .env + expo-dotenv / SDK-native .env support

Expo 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.

Which to choose

  • If the project already has .envOption B.
  • If the project hasn't set up env vars at all — Option A (one file, no prefix rules).
  • Never mix both for the same variable — pick one place.

---

Step 5 — Expo Router subsection

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.

5a — Router entry (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>
  );
}

5b — Conversations + message route

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>
  );
}

5c — Tabs in Expo Router (if the project uses them)

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.

Expo Router gotchas

  • `unstable_settings` in _layout.tsx can break deep linking if set incorrectly. Leave it alone unless you know you need it.
  • Navigation between stack + tabs: router.push("/messages/abc") works from a tab. router.back() returns to the tab. No extra configuration needed.
  • Search params: use useLocalSearchParams() (not useSearchParams() — that's web-only).

---

Step 6 — Prebuild + run

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:

  • Changed JS / JSX / TSX → no rebuild, just r to reload or save in Fast Refresh
  • Added / removed a native dependency → npx expo prebuild --clean && npx expo run:ios
  • Changed app.json permissions or plugins → npx expo prebuild --clean

---

Step 7 — Verify integration

npx tsc --noEmit    # TypeScript check

Then in the running app:

1. Open the chat screen you wired 2. Check that the keyboard opens when you tap the composer (gesture handler working) 3. Tap the "+" attachment button — the action sheet should slide up (bottom sheet working) 4. Send a message — it should appear immediately

If any of these don't work, see cometchat-native-troubleshooting.

---

Hard rules

1. No Expo Go. The user's project must use development builds. Detect early and tell the user if they're on Expo Go. 2. `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. 3. Install all peer deps via `npx expo install`, not npm install, for native modules. Expo's resolver picks SDK-compatible versions. 4. Never commit `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). 5. Merge permissions into `app.json`, don't replace. The user may already have permissions for other libraries; wipe them out and their other features break. 6. `npx expo prebuild --clean` after changing `app.json` permissions or adding native deps. Without it, iOS + Android see the old config. 7. Every `<CometChatMessageList>` must include `hideReplyInThreadOption` unless you're also wiring a full thread panel (see cometchat-native-placement § Hard rule 5). 8. The four-wrapper chain goes at the app root, not per-screen (see cometchat-native-core § 3). For Expo Router, that's app/_layout.tsx. For plain Expo, that's App.tsx.

---

Common questions

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 routing reference

SkillWhen to route
cometchat-native-coreInit / login / wrapper chain / anti-patterns
cometchat-native-componentsComponent prop reference
cometchat-native-placementWhere chat goes (stack / tabs / modal / bottom sheet / embedded)
cometchat-native-expo-patternsThis skill — Expo managed workflow specifics
cometchat-native-bare-patternsBare RN (pod install, native modules, privacy manifest)
cometchat-native-featuresCalls, extensions, AI
cometchat-native-themingTheme customization
cometchat-native-customizationText formatters, events, custom views
cometchat-native-productionServer-side auth tokens + user management
cometchat-native-troubleshootingPrebuild failures, Expo Go errors, keyboard issues, blank chat

Visual Builder integration (v4.3)

If the customer picks Visually in dispatcher Step 3.1, the Expo recipe diverges from the standard provider chain. Skills runs cometchat builder export --platform react-native --json to emit src/config/{store.ts, config.json} (the Zustand-backed config store + 7-field envelope JSON), then patches App.tsx with useConfig + theme derivation.

Full recipe lives in `cometchat-native-core` §"Visual Builder integration". Expo-specific notes:

  • Use npx expo install for the deps (not raw npm install) — Expo SDK pins compatible versions.
  • Env via process.env.EXPO_PUBLIC_COMETCHAT_* (no extra Babel plugin needed; Expo handles this).
  • Required additional deps beyond the standard provider set: zustand, @react-native-async-storage/async-storage.
  • useConfig(s => s.settings.style) — note the selector takes AppConfig directly, NOT s.config.settings.style (the .config. wrapper is internal to the Zustand store — Finding F6, 2026-05-21).
  • A2 smoke validated: Metro bundles 10.8 MB iOS export clean on Expo SDK 54.

If the customer picks In code, ignore this section — the standard four-wrapper chain + provider pattern from §"Provider chain" applies.

Related skills

Mobile Developmentfrontendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.