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]);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.
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).
<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" /><key>NSCameraUsageDescription</key>
<string>We need camera access for video calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access for audio calls.</string>export default {
ios: {
infoPlist: {
NSCameraUsageDescription: "We need camera access for video calls.",
NSMicrophoneUsageDescription:
"We need microphone access for audio calls.",
},
},
android: {
permissions: [
"CAMERA",
"RECORD_AUDIO",
"POST_NOTIFICATIONS",
"BLUETOOTH_CONNECT",
],
},
};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.
<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" />export default {
android: {
permissions: [
"POST_NOTIFICATIONS",
"FOREGROUND_SERVICE",
"FOREGROUND_SERVICE_CAMERA",
"FOREGROUND_SERVICE_MICROPHONE",
"FOREGROUND_SERVICE_CONNECTED_DEVICE",
"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 secondsMore: 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.