Integration Best Practices

After the integration is complete and you prepare to deploy your app, review the following best practices. This page highlights areas to pay attention to when integrating the Stream Video React Native SDK to ensure a smooth user experience.

Client and Call initialization

client and call instances are stateful objects that must be managed carefully. Failing to do so can cause memory leaks, performance issues, or unexpected behavior (for example, a call isn’t disposed and the user continues publishing audio/video).

Create client and call inside a useEffect hook and dispose of them when no longer needed or when the component unmounts.

StreamVideoClient

Create a single StreamVideoClient instance in your app. Multiple instances can break push notifications and call state management. Use StreamVideoClient.getOrCreateInstance(...) to ensure a singleton.

const [client, setClient] = useState<StreamVideoClient>();
useEffect(() => {
  const tokenProvider = async () => api.fetchToken(user.id);
  const client = StreamVideoClient.getOrCreateInstance({
    apiKey,
    user,
    tokenProvider,
  });
  setClient(client);
  return () => {
    client.disconnectUser().catch((err) => console.error(err));
    setClient(undefined);
  };
}, [apiKey, user.id]);

Token and Token Providers

Use a tokenProvider with short-lived tokens (around 4 hours).

More: Client & Authentication.

Call

call can be created only after the StreamVideoClient is initialized.

import { useStreamVideoClient } from "@stream-io/video-react-native-sdk";

const client = useStreamVideoClient();
const [call, setCall] = useState<Call>();

useEffect(() => {
  if (!client) return;
  const call = client.call(type, id);
  setCall(call);
  call.join().catch((err) => console.error(err));
  return () => {
    // dispose the call once you don't need it anymore
    call.leave().catch((err) => console.error(err));
    setCall(undefined);
  };
}, [client, type, id]);

In your UI components, use the useCall() hook to access the call instance exposed by the <StreamCall call={call} /> provider.

import { useCall } from "@stream-io/video-react-native-sdk";

export const MyComponent = () => {
  const call = useCall();
  useEffect(() => {
    if (!call) return;
    call.getOrCreate().catch((err) => console.error(err));
  }, [call]);
};

A call is considered initialized after one of the following operations:

  • await call.get()
  • await call.create()
  • await call.getOrCreate()
  • await call.join()

Always dispose of the call instance by calling call.leave() when you no longer need it. Dangling call instances cause memory leaks and unexpected behavior.

More: Joining & Creating Calls.

Audio routing lifecycle

On React Native, audio routing is managed by callManager. Make sure you start it when joining a call and stop it when leaving.

import { callManager } from "@stream-io/video-react-native-sdk";

// Before joining a call (or immediately after joining)
callManager.start({
  audioRole: "communicator",
  deviceEndpointType: "speaker",
});

// When leaving a call
callManager.stop();

More: Camera & Microphone.

Calling State

The call instance has multiple states. Make sure you handle all possible states and show appropriate UI to the user.

import { useCallStateHooks } from "@stream-io/video-react-native-sdk";

const { useCallCallingState } = useCallStateHooks();
const callingState = useCallCallingState();

More: Calling State and Lifecycle.

Device management

Native permission prompt

Mobile platforms require explicit native permissions for camera, microphone, Bluetooth, and notifications. Request these permissions at an appropriate point in your flow so users are not surprised by system prompts. Make sure the platform-specific permission declarations are in place for React Native CLI (AndroidManifest.xml, Info.plist) or Expo (app config plugins).

android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
ios/Info.plist
<key>NSCameraUsageDescription</key>
<string>We need camera access for video calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access for audio calls.</string>
import { Platform } from "react-native";
import { PERMISSIONS, requestMultiple } from "react-native-permissions";

export const requestAndUpdatePermissions = async () => {
  if (Platform.OS === "ios") {
    await requestMultiple([PERMISSIONS.IOS.CAMERA, PERMISSIONS.IOS.MICROPHONE]);
  } else if (Platform.OS === "android") {
    await requestMultiple([
      PERMISSIONS.ANDROID.CAMERA,
      PERMISSIONS.ANDROID.RECORD_AUDIO,
      PERMISSIONS.ANDROID.BLUETOOTH_CONNECT,
      PERMISSIONS.ANDROID.POST_NOTIFICATIONS,
    ]);
  }
};

export const hasCameraPermission = async () => {
  const permission =
    Platform.OS === "ios" ? PERMISSIONS.IOS.CAMERA : PERMISSIONS.ANDROID.CAMERA;
  return (await check(permission)) === RESULTS.GRANTED;
};

export const hasMicPermission = async () => {
  const permission =
    Platform.OS === "ios"
      ? PERMISSIONS.IOS.MICROPHONE
      : PERMISSIONS.ANDROID.RECORD_AUDIO;
  return (await check(permission)) === RESULTS.GRANTED;
};

More: Manage Native Permissions.

Camera and audio route switching

For cameras, users often need a quick way to flip between front and back cameras.

import { useCallStateHooks } from "@stream-io/video-react-native-sdk";

const { useCameraState } = useCallStateHooks();
const { camera } = useCameraState();

camera.flip();

For audio routes (speaker, earpiece, Bluetooth, wired), follow the platform-specific APIs from our camera & microphone guide.

More: Camera & Microphone.

Lobby

Provide a lobby screen so users can check devices and adjust settings before joining.

More: Lobby Preview.

Speaking while muted detection

The SDK can detect when the user is speaking while muted. Show an indicator and consider allowing users to disable the feature.

import { useCallStateHooks } from "@stream-io/video-react-native-sdk";

const { useMicrophoneState } = useCallStateHooks();
const { isSpeakingWhileMuted, microphone } = useMicrophoneState();

if (isSpeakingWhileMuted) {
  console.log("You are speaking while muted!");
}

await microphone.disableSpeakingWhileMutedNotification();

Background/foreground behavior

When the app goes to the background, Android requires a foreground service to keep the call alive. Declare the required permissions and request notification permissions on Android 13+. If you use Expo, prefer the Expo configuration and guides; if you use React Native CLI, update native files directly.

android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

More: Keeping a Call Alive.

Incoming calls and push notifications

For ringing calls, make sure your push setup is complete so users can accept or decline calls when the app is backgrounded or terminated. Follow the incoming calls overview and the platform-specific ringing setup.

More: Incoming Calls Overview and Ringing.

Audio and Video filters

Noise cancellation and background filters add CPU overhead and may reduce performance on low-end devices. We automatically disable noise cancellation under CPU pressure, but you should still provide UI to turn these off if users experience issues.

More: Noise Cancellation and Video Filters.

Error handling

The SDK is asynchronous, so handle promise rejections and surface meaningful error messages.

Device errors

try {
  await call.camera.enable();
  await call.microphone.enable();
} catch (err) {
  console.error("Failed to enable a device", err);
}

Join errors

try {
  await call.join();
} catch (err) {
  console.error("Failed to join the call", err);
}

More: Troubleshooting.

Join retries

call.join() retries by default with exponential backoff. Tune maxJoinRetries if you want to fail fast or provide custom retry UX.

try {
  await call.join({ maxJoinRetries: 1 });
} catch (err) {
  console.error("Join failed", err);
}

More: Troubleshooting.

Connect user issues

const client = StreamVideoClient.getOrCreateInstance({ apiKey });
try {
  await client.connectUser(user, token);
} catch (err) {
  console.error("Failed to connect user", err);
}

More: Client & Authentication.

Network

Firewall and proxy setup

Some networks may block WebRTC traffic. If your users are behind a restrictive network, the SDK may fail to connect. If you control the network, apply the settings from the networking guide; otherwise, inform users to switch networks.

More: Networking and Firewall.

Reconnections

When switching networks (Wi-Fi to cellular) the SDK attempts to reconnect. Avoid immediately ending calls when a participant temporarily leaves. Use calling state and disconnection timeouts instead.

More: Network Disruptions.

Disconnection timeout

Set a disconnection timeout to define how long a user can stay disconnected before being removed from the call.

call.setDisconnectionTimeout(30); // Try to reconnect for 30 seconds

More: Network Disruptions.

Low bandwidth

If bandwidth is low, the SDK may pause incoming video. Use indicators to inform users when this happens.

More: Low Bandwidth.

Single-call concurrency

Guard against users receiving or starting multiple calls at once. Consider enabling auto-reject for busy users and show a clear “busy” UX.

const client = StreamVideoClient.getOrCreateInstance({
  apiKey,
  tokenProvider,
  user,
  options: { rejectCallWhenBusy: true },
});

More: Reject Call When Busy.

User permissions

Review role permissions in the dashboard; hiding UI is not sufficient since users could call APIs directly.

More: Permissions & Moderation.

Gather feedback

Collect user feedback to improve call quality and understand user needs.

More: User Ratings.

Supported platforms and testing

Verify your React Native or Expo version requirements, and test on real devices. Audio/video recording is not supported on iOS simulators.

More: React Native Installation and Expo Installation.

Keep dependencies up-to-date

We regularly release improvements and fixes. Keep your SDK dependencies updated and review changes as they ship.

We provide detailed change logs in GitHub Releases.