# Troubleshooting

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

This page collects common problems and what causes them. Coming from v9? Many problems after an upgrade are covered in [Upgrade to v10](https://getstream.io/chat/docs/sdk/react-native/v10/release-guides/upgrade-to-v10/). For a problem that isn't listed here, search the [SDK's GitHub issues](https://github.com/GetStream/stream-chat-react-native/issues).

## Context providers

The SDK's components share values through React context, so a component reads the client, the channel or the message from the provider above it. [How components connect to state](https://getstream.io/chat/docs/sdk/react-native/v10/ui/how-components-connect-to-state/) lists which component provides what.

A hook whose provider is required throws when it's used outside that provider, and the error names the hook and the provider. For example, `useChatContext()` outside `Chat` throws an error that starts with:

```text
The useChatContext hook was called outside the ChatContext Provider.
```

Move the component inside the provider. The usual cases are a custom component rendered outside the message list, a loading or empty state rendered outside `Chat`, and a screen rendered outside `OverlayProvider`. `useChannel()`, `useChannelContext()` and `useMessagePaginator()` throw outside a `Channel` or a `Thread`, and so do the hooks built on them. A component that renders both inside and outside a channel, such as a channel-list row, takes the channel as a prop instead. [Context hooks that throw](https://getstream.io/chat/docs/sdk/react-native/v10/ui/reading-state-in-components/#context-hooks-that-throw) has the details.

## A channel shows no messages, or doesn't update

`Channel` loads its channel when it mounts. It watches the channel with `Channel.watch()`, which loads the latest messages and subscribes to the channel's events, so a channel from `client.channel()` needs no request before you pass it to `Channel`. See [Channel UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/channel-ui/).

- **The list shows an error.** When the first page of messages fails to load, `Channel` renders `LoadingErrorIndicator` in place of its children, with a retry button that reloads the latest messages. Check the network and the error it shows.
- **The list shows messages, but new ones don't arrive.** With `initializeOnMount={false}`, `Channel` doesn't watch the channel. It still loads a page of messages when it has none, but the channel receives no events. Call `Channel.watch()` yourself.

`channel` is a required prop. `<Channel>` without a channel throws, so render your own placeholder while no channel is selected.

## A thread renders nothing, or throws

`Thread` shows the thread passed as its `thread` prop. `<Thread>` without one throws `TypeError: Cannot read property 'messagePaginator' of undefined`. Pass the thread, and render its parts as children:

```tsx
import type { Thread as StreamThread } from "stream-chat";
import { MessageComposer, MessageList, Thread } from "stream-chat-react-native";

export const ThreadScreen = ({ thread }: { thread: StreamThread }) => (
  <Thread thread={thread}>
    <MessageList />
    <MessageComposer />
  </Thread>
);
```

Render `Thread` on its own screen, or next to `Channel`, not inside it. Get the thread from the `onThreadSelect` callback of `MessageList` or `ThreadList`, from `client.threads.get(id)` once it's open, or from `client.threads.ensure({ channel, parentMessage })`. Don't create it with `new Thread()`. `client.threads.ensure()` returns the one instance the client keeps up to date.

`Thread` loads the first page of replies when it mounts. If that fails, it renders `LoadingErrorIndicator` with a retry button. See [Thread UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/thread-ui/).

## Tapping a channel or a thread does nothing

The SDK doesn't navigate. The channel list, the replies button and the thread list call back with what the user picked, and your navigator shows it. Without the callback, a tap does nothing:

- A channel-list row calls `onSelect` on `ChannelList` with the channel.
- The replies button and the "Thread Reply" action of a message call `onThreadSelect` on `MessageList` with the `Thread`.
- A thread-list row calls `onThreadSelect` on `ThreadList` with the `Thread`.

Navigate to the channel or thread screen from these callbacks. The [Quick start](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/quick-start/) and [Navigation](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/navigation/) show how with React Navigation and Expo Router.

## A channel list shows the first page and never updates

A `ChannelPaginator` stays current only while it's registered on `client.channelManager`, which routes events into the lists it holds. A paginator that's only passed to `ChannelList` loads its first page, and then never adds, moves or removes a channel when messages arrive:

```ts
client.channelManager.insertPaginator({ paginator });
```

Remove it with `client.channelManager.removePaginator(paginator)` when the list goes away. `ChannelList` never registers or disposes the paginator it renders. [Channel list paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/channel-list-paginators/) explains both.

## A component re-renders too often, or misses changes

`useStateStore` follows three rules, and each mistake shows a different symptom:

- **A selector defined inline** is a new function on every render, so the hook unsubscribes and subscribes again each time. Define it at module scope, or wrap it in `useCallback` when it needs a prop.
- **A selector that returns the slice itself**, such as `(state) => state.members`, compiles but misses changes. Return an object with fixed keys: `(state) => ({ members: state.members })`.
- **A selector that maps or filters** builds a new array on every run, so the component re-renders on every change to the store. Select the fields as they are and compute from them in the component.

```tsx
import { Text } from "react-native";
import type { ChannelStateData } from "stream-chat";
import { useChannel, useStateStore } from "stream-chat-react-native";

// Declared at module scope, so the hook subscribes once.
const typingSelector = (state: ChannelStateData) => ({ typing: state.typing });

export const TypingCount = () => {
  const channel = useChannel();
  const { typing } = useStateStore(channel.state, typingSelector);
  return <Text>{Object.keys(typing).length}</Text>;
};
```

Getters such as `channel.state.members` and `channel.data` return the value at the moment you read them, and don't tell you when it changes, so a component that reads them doesn't re-render. [The useStateStore rules](https://getstream.io/chat/docs/sdk/react-native/v10/ui/reading-state-in-components/#the-usestatestore-rules) explains each rule.

## Translations stopped applying

In v10 a translation key is a dotted identifier, such as `messageInput.sendMessage.accessibilityLabel`, not the English copy. A dictionary still keyed on v9 keys never matches, and the SDK renders its English copy, with no error. Rename the keys, and type the dictionary as `TranslationDictionary` so TypeScript flags any you missed. See [Translation keys are dotted identifiers](https://getstream.io/chat/docs/sdk/react-native/v10/release-guides/upgrade-to-v10/#translation-keys-are-dotted-identifiers) in the upgrade guide and [Localization](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/).

## Dates are blank or show "Invalid Date"

Timestamps from the server are nanosecond numbers. A date library reads a number as milliseconds, so `dayjs(message.created_at)` is an invalid date and renders "Invalid Date". TypeScript doesn't catch that, because a timestamp is a `number`. `new Date(message.created_at)` is marked deprecated, and using its result as a `Date` doesn't compile. The SDK's date formatters return `null` for an invalid date, so a timestamp passed to them unconverted shows as a blank. Convert first, with `nsToDate()` or `convertTimestampToDate()` from `stream-chat`:

```ts
import dayjs from "dayjs";
import { nsToDate } from "stream-chat";
import type { LocalMessage } from "stream-chat";

export const formatSentAt = (message: LocalMessage) =>
  dayjs(nsToDate(message.created_at)).format("LT");
```

[Data conventions](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/data-conventions/) lists the helpers.

## A deleted channel stays on screen

When a channel is deleted, the client disconnects its channel object and removes it from `client.channelManager`. `Channel` keeps rendering the messages it has, so a user who has the channel open stays on a channel that no longer exists. Listen for the event on the channel screen and navigate away:

```tsx
import { useEffect } from "react";
import type { Channel as StreamChannel } from "stream-chat";
import { useChatContext } from "stream-chat-react-native";

export const useLeaveWhenDeleted = (
  channel: StreamChannel,
  leave: () => void,
) => {
  const { client } = useChatContext();

  useEffect(() => {
    const deleted = client.on("channel.deleted", (event) => {
      if (event.cid === channel.cid) leave();
    });
    const removed = client.on("notification.channel_deleted", (event) => {
      if (event.cid === channel.cid) leave();
    });
    return () => {
      deleted.unsubscribe();
      removed.unsubscribe();
    };
  }, [channel, client, leave]);
};
```

Pass a stable `leave`, such as `navigation.goBack` or a function wrapped in `useCallback`. A disconnected channel object never comes back: to open the conversation again, get a new one with `client.channel(type, id)`. [A deleted channel keeps rendering](https://getstream.io/chat/docs/sdk/react-native/v10/release-guides/upgrade-to-v10/#a-deleted-channel-keeps-rendering) shows how to read the channel's `pendingDisposal` state instead.

## "No connection id is available"

Watching a channel and subscribing to presence are both server-side subscriptions, and the server keys them to your WebSocket by its connection id. A request carrying either is therefore held until the handshake has produced one, rather than sent to a server that would answer `200` and register nothing.

Usually the request waits, the socket connects, and the request goes out, so you never see this. The error appears only when no amount of waiting would help, because there is no socket and none is being opened:

```text
No connection id is available: there is no WebSocket connection, and none is being established. A request that watches a channel or subscribes to presence needs one. Call `client.connectUser()` if no user is connected, or `client.openConnection()` if the socket was closed with `client.closeConnection()`.
```

Two situations reach it. No user was connected, which in tests usually means a fixture that fakes a connected user without a connection. Or the socket was closed with `client.closeConnection()`, and nothing has reopened it. `Chat` closes the socket when the app goes to the background, and opens it again when the app comes back to the foreground. A request that watches a channel while the app is in the background, for example from a push notification handler, gets this error. Set `closeConnectionOnBackground={false}` on `Chat` to keep the socket open in the background. For apps created before 2022-01-18, the server sends push notifications only while the user has no open socket; newer apps get them whether the user is online or not.

A request that's waiting can be abandoned. The abort signal you pass reaches the wait, not only the HTTP call:

```ts
const controller = new AbortController();
client.queryChannels({ filter_conditions: {} }, { signal: controller.signal });
controller.abort();
```

A query that asks for neither a watch nor presence is never held, so it works while the socket is down:

```ts
client.queryChannels({ filter_conditions: {}, presence: false, watch: false });
```

See [Connection](https://getstream.io/chat/docs/sdk/react-native/v10/state/connection/) for reading the socket's status directly.

## User roles and permission policies

If a user can do something they shouldn't, or can't do something they should, check their roles and the channel type's permissions.

Every channel has a type, such as `livestream`, `messaging`, `team`, `gaming` or `commerce`, set when the channel is created. Each type comes with a default permission policy, which you can change. A policy allows or denies a role access to an action. For example, the `admin` role is allowed to create a channel and to ban a user.

The UI reads the result as the user's capabilities in the channel, and components hide what the user can't do. Inside a `Channel` or a `Thread`, `useOwnCapabilitiesContext()` returns them as booleans, such as `sendReply`. Anywhere else, `useChannelOwnCapabilities(channel)` returns the channel's capability names. `overrideOwnCapabilities` on `Channel` changes what the UI shows, not what the server allows. Change permissions on the server, not in the UI. See [Permission policies](https://getstream.io/chat/docs/node/chat-permission-policies/) and the [Permissions reference](https://getstream.io/chat/docs/javascript/permissions-reference/).

You can change the permission policies of a channel type in the [Stream Dashboard](https://getstream.io/signin/?product=chat). You can also make a user an admin or a moderator through the API, but only from your server.

## Watching channels

Watching a channel loads it and subscribes this connection to its events: new messages, member changes, typing and the rest. `Channel` watches its channel when it mounts, so you call these yourself only for a channel you use outside `Channel`, or one rendered with `initializeOnMount={false}`.

- `Channel.watch()` creates the channel if it doesn't exist, loads it with its latest messages and watches it. Calling it again is safe, even from several places at once. On a channel that's already watched and loaded, it sends a request only when you pass options it wasn't watched with, and a call made while the same watch is in flight joins it. Use the channel it resolves with. To refresh a watched channel, call `Channel.reload()`.
- `Channel.create()` creates the channel without watching it.
- `Channel.query()` returns the channel's data and updates its state, but doesn't add messages to its message list. With `watch: true` it also watches the channel. To show a channel's messages, use `Channel.watch()`.

See [watch and query](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/shared-entities/#watch-and-query).

## Watcher vs member

A watcher is a user subscribed to a channel's updates, such as new messages. Watching doesn't need membership: a user who isn't a member can watch a `livestream` channel, for example.

A member belongs to the channel, and usually has more permissions than a user who isn't one. For example, the default permissions of the `messaging` type let members read and write in the channel, and receive push notifications.

## Errors after installing or upgrading

- The SDK's peer dependencies and most of its optional packages are native modules. A JavaScript reload doesn't load them, so rebuild the app after adding one. Run `npx pod-install` and rebuild for React Native CLI, or `npx expo prebuild` and rebuild for an Expo app that uses prebuild.
- When modules resolve to old code or fail to resolve, clear the Metro cache with `npx react-native start --reset-cache`, or `npx expo start -c`.
- The SDK and your app must use the same copy of `stream-chat`. With two, the client and the objects your app creates come from different classes. List the copies with `npm ls stream-chat` or `yarn why stream-chat`, and install `stream-chat` at the version the SDK uses. See [Installation](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/installation/).
- Expo Go can't load native packages that aren't part of it, such as `react-native-teleport` and `@op-engineering/op-sqlite`. Use a [development build](https://docs.expo.dev/develop/development-builds/introduction/).

## Errors about worklets

The SDK animates with `react-native-reanimated`. Errors that mention worklets, such as a worklet that couldn't be created, usually mean the Babel plugin is missing or isn't the last plugin in `babel.config.js`. With `react-native-reanimated` 4 the plugin is `react-native-worklets/plugin`, and with version 3 it's `react-native-reanimated/plugin`:

```js title="babel.config.js"
module.exports = {
  presets: [
    // your presets
  ],
  plugins: [
    // your other plugins
    "react-native-worklets/plugin", // must be listed last
  ],
};
```

Clear the Metro cache after changing the Babel config. See the [Reanimated installation guide](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/).

## Touches and gestures don't work

The SDK's gestures, such as swiping a message to reply, swiping a channel-list row and paging through the image gallery, come from `react-native-gesture-handler`. They work only inside a `GestureHandlerRootView`. Wrap the root of your app in it, above `OverlayProvider`:

```tsx title="App.tsx"
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { OverlayProvider } from "stream-chat-react-native";

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <OverlayProvider>{/* Chat and your screens */}</OverlayProvider>
    </GestureHandlerRootView>
  );
}
```

On Android, the content of a React Native `Modal` renders outside the root view, so wrap it in its own `GestureHandlerRootView` too.

A pressable inside a custom message or channel-list row sits inside the row's swipe gesture. If it doesn't receive its taps, or the wrong element does, check whether the gesture is the cause: turn swiping off with `enableSwipeToReply={false}` on `Channel`, or `swipeActionsEnabled={false}` on `ChannelList`. Nested gestures behave differently on iOS and Android, so test both. The [Gesture Handler docs](https://docs.swmansion.com/react-native-gesture-handler/docs/) cover how gestures and touchables interact.

## The composer is hidden by the keyboard

`Channel` and `Thread` wrap their content in `KeyboardCompatibleView`, which moves the composer above the keyboard. It compares the keyboard's position on the screen with its own layout, so it needs to know how much is drawn above it. Pass that height as `keyboardVerticalOffset`: the height of the navigation header, or `0` when nothing is above the channel. Use the same value on iOS and Android, and on every screen that renders the channel. When the prop is left out, `KeyboardCompatibleView` applies its own default offset (86.5 on iOS, -300 on Android) instead of `0`.

```tsx
import { useHeaderHeight } from "@react-navigation/elements";
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageList,
} from "stream-chat-react-native";

export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => {
  const headerHeight = useHeaderHeight();

  return (
    <Channel channel={channel} keyboardVerticalOffset={headerHeight}>
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};
```

On Android, `KeyboardCompatibleView` detects whether the app draws edge to edge, which apps targeting Android 15 (API 35) do by default. An app that doesn't is resized by the system when the keyboard opens, and the view adds no offset of its own. An app that does isn't resized, and the view moves the composer itself. Remove inset listeners in `MainActivity` and other keyboard workarounds added for older SDK versions, and test again with only `keyboardVerticalOffset`.

To handle the keyboard yourself, set `disableKeyboardCompatibleView` on `Channel`. See [Keyboard and insets](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/keyboard-and-insets/).

## The attachment picker opens at the wrong height

The attachment picker is a bottom sheet that opens below the composer. When something is drawn below the channel, such as a tab bar or the bottom safe-area inset, pass its height to `Channel` as `bottomInset`, so the picker and the message list account for it. When the channel is below a navigation header, pass the header's height as `topInset`, so the open picker stays below it:

```tsx
<Channel bottomInset={tabBarHeight} channel={channel} topInset={headerHeight}>
  {/* ... */}
</Channel>
```

Both default to `0`. `useAttachmentPickerContext()` returns the values in use, and only `Channel` and `Thread` set them. See [Keyboard and insets](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/keyboard-and-insets/).

## The message menu or the image gallery doesn't cover the screen

`OverlayProvider` draws the message menu and the image gallery over everything inside it. If they open below a header or above a tab bar, `OverlayProvider` is placed too low in the tree, for example inside one screen. Place it once, above your navigator and your screens, as the [Quick start](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/quick-start/) does.

## The message menu doesn't show inside a modal

If a long press on a message inside a modal hides the message without showing the menu, the menu is drawn behind the modal. On iOS, a modal presented natively, for example by a native stack navigator, is drawn in front of the root `OverlayProvider`. Give the chat inside the modal its own `OverlayProvider`. See [Chat in modals](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/chat-in-modals/).

## The camera or the photo library doesn't open

Each picker needs its package and the platform permissions:

- The built-in media library needs `@react-native-camera-roll/camera-roll` (React Native CLI) or `expo-media-library` (Expo). Without it, the attachment picker doesn't show its own image grid. On iOS, it needs `NSPhotoLibraryUsageDescription`, and `NSPhotoLibraryAddUsageDescription` to save images. On Android, it needs `READ_MEDIA_IMAGES` and `READ_MEDIA_VIDEO`, and `READ_EXTERNAL_STORAGE` up to API 32.
- The camera needs `react-native-image-picker` (React Native CLI) or `expo-image-picker` (Expo). On iOS, it needs `NSCameraUsageDescription`, and `NSMicrophoneUsageDescription` to record videos. On Android, it needs the `CAMERA` permission.

[Installation](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/installation/#optional-features) shows each package with its permissions, for React Native CLI and for Expo config plugins.

## Google Play rejects the photo and video permissions

Google Play's [photo and video permissions policy](https://support.google.com/googleplay/android-developer/answer/14115180) allows `READ_MEDIA_IMAGES` and `READ_MEDIA_VIDEO` only for apps that need broad access to the user's photos and videos. The built-in media library needs them. Remove `@react-native-camera-roll/camera-roll` or `expo-media-library`, and the two permissions, and install `react-native-image-picker` or `expo-image-picker`. The attachment picker then opens the system picker, which needs no Android permission. Check the merged manifest for packages that still add them. In an Expo app, list them under `android.blockedPermissions` in `app.json`. See [Built-in media library](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/installation/#built-in-media-library).

## Links don't open on Android

The SDK opens a link only after `Linking.canOpenURL()` confirms that an app can handle it. Apps that target Android 11 (API 30) or later see other apps only when they declare it, so `canOpenURL()` returns `false` for web links until the manifest declares them. Add this to `AndroidManifest.xml`:

```xml title="AndroidManifest.xml"
<queries>
  <intent>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="http" android:host="*" />
  </intent>
  <intent>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="https" android:host="*" />
  </intent>
</queries>
```

## GIFs and WebP images don't show on Android

The SDK renders images with React Native's `Image`, which shows GIFs and WebP images on Android only with extra Fresco modules. In a React Native CLI app, add them to `android/app/build.gradle`, at the Fresco version your React Native version uses. The version is `fresco` in `node_modules/react-native/gradle/libs.versions.toml`:

```groovy title="android/app/build.gradle"
dependencies {
  // Animated GIFs
  implementation("com.facebook.fresco:animated-gif:3.7.0")
  // WebP, and animated WebP
  implementation("com.facebook.fresco:animated-webp:3.7.0")
  implementation("com.facebook.fresco:webpsupport:3.7.0")
}
```

An Expo app adds them through `android/gradle.properties`: `expo.gif.enabled` and `expo.webp.enabled` are `true` in a new project, and `expo.webp.animated` turns on animated WebP.

## op-sqlite doesn't build with static frameworks

With offline support, `@op-engineering/op-sqlite` can fail to build when the Podfile uses `use_frameworks! :linkage => :static`, which some libraries, such as React Native Firebase, require. The errors look like these:

```text
error: Multiple commands produce '<some-path>/op-sqlite/op_sqlite.framework/Headers/libsql.h'
include of non-modular header inside framework module
```

Build `op-sqlite` as a static library instead. In a React Native CLI app, add this inside the app's `target` in `ios/Podfile`:

```ruby title="ios/Podfile"
pre_install do |installer|
  installer.pod_targets.each do |pod|
    if pod.name.eql?('op-sqlite')
      def pod.build_type
        Pod::BuildType.static_library
      end
    end
  end
end
```

In an Expo app, add the same block through a config plugin, listed last in `app.json` so it runs after the other plugins. The `ExpoMessaging` example app has one in [`plugins/opSqliteSwiftPlugin.js`](https://github.com/GetStream/stream-chat-react-native/blob/develop/examples/ExpoMessaging/plugins/opSqliteSwiftPlugin.js). See the [op-sqlite installation docs](https://op-engineering.github.io/op-sqlite/docs/installation) and [this GitHub issue](https://github.com/GetStream/stream-chat-react-native/issues/2926) for more context.

## Notifee doesn't build in an Expo app

If Notifee fails to build on Android in an Expo app, Gradle doesn't find Notifee's own Maven repository. Add it with the `expo-build-properties` config plugin in `app.json`:

```json title="app.json"
{
  "expo": {
    "plugins": [
      [
        "expo-build-properties",
        {
          "android": {
            "extraMavenRepos": [
              "$rootDir/../node_modules/@notifee/react-native/android/libs"
            ]
          }
        }
      ]
    ]
  }
}
```

The path is relative to the `android` folder. In a monorepo, point it at the `node_modules` folder that holds `@notifee/react-native`.

## React Native Firebase doesn't build on Expo 54

React Native Firebase needs static frameworks on iOS. On Expo 54, turn them on with the `expo-build-properties` config plugin, and link the Firebase modules you use statically:

```json title="app.json"
{
  "expo": {
    "plugins": [
      [
        "expo-build-properties",
        {
          "ios": {
            "useFrameworks": "static",
            "forceStaticLinking": ["RNFBApp", "RNFBMessaging"]
          }
        }
      ]
    ]
  }
}
```

With offline support, static frameworks also need the [op-sqlite fix](#op-sqlite-doesnt-build-with-static-frameworks). For push notifications, see [Push notifications](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/push-notifications/).

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/troubleshooting/](https://getstream.io/chat/docs/sdk/react-native/v10/troubleshooting/).