Integration Best Practices

Review these best practices before deploying your Stream Video React Native SDK integration.

Client and Call initialization

client and call are stateful objects requiring careful management. Poor handling causes memory leaks, performance issues, or unexpected behavior (e.g., undisposed calls continue publishing audio/video).

Create instances in useEffect and dispose on unmount.

StreamVideoClient

Create one StreamVideoClient instance per app. Multiple instances break push notifications and state management. Use getOrCreateInstance() for singleton pattern.

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 tokenProvider with ~4 hour tokens. See Client & Authentication.

Client behavior consistency - For incoming calls, use identical options in your app flow and StreamVideoRN.setPushConfig since getOrCreateInstance reuses cached instances.

Call

Create call only after StreamVideoClient initializes.

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

Access call via useCall() hook within <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]);
};

Call initializes after:

  • 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

callManager handles audio routing. Start when joining, stop 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

Handle all call states with appropriate UI:

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 camera, microphone, Bluetooth, and notification permissions.

  • Declare permissions in AndroidManifest.xml/Info.plist (CLI) or app config plugins (Expo)
  • Request at appropriate points to avoid surprising users

See Manage Native Permissions.

Camera and audio route switching

Flip cameras:

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

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

camera.flip();

For audio routes, see Camera & Microphone.

Lobby

Provide a lobby for device checks before joining. See Lobby Preview.

Speaking while muted detection

Detect and notify when users speak while muted:

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

Android requires a foreground service for background calls. Declare permissions and request notifications on Android 13+.

See Keeping a Call Alive.

Incoming calls and push notifications

Complete push setup for ringing calls when app is backgrounded/terminated.

See Incoming Calls Overview and Ringing.

Audio and Video filters

Noise cancellation and background filters add CPU overhead. SDK auto-disables under pressure, but provide manual toggle for low-end devices.

See Noise Cancellation and Video Filters.

Error handling

Handle promise rejections and surface meaningful errors.

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 with exponential backoff. Adjust maxJoinRetries for custom behavior:

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

Restrictive networks may block WebRTC. Apply Networking and Firewall settings or advise users to switch networks.

Reconnections

SDK auto-reconnects on network changes. Use calling state and disconnection timeouts instead of ending calls on temporary disconnects.

See Network Disruptions.

Disconnection timeout

Define reconnection window before removal:

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

More: Network Disruptions.

Low bandwidth

SDK pauses video on low bandwidth. Show indicators. See Low Bandwidth.

Single-call concurrency

Prevent multiple concurrent calls. Enable auto-reject for busy users:

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

More: Reject Call When Busy.

User permissions

Configure role permissions in dashboard - hiding UI alone is insufficient. See Permissions & Moderation.

Gather feedback

Collect feedback to improve quality. See User Ratings.

Supported platforms and testing

Test on real devices - iOS simulators lack camera/microphone support.

See React Native Installation and Expo Installation.

Keep dependencies up-to-date

Update SDK regularly. See GitHub Releases for changelogs.