Skip to content
/Building Real-Time Chat with WebSocket in React Native
Jan 2024·6 min read

Building Real-Time Chat with WebSocket in React Native

Lessons from building a production WebSocket group chat supporting 200+ concurrent sessions — covering connection lifecycle, offline queuing, message deduplication, swipe-to-reply, and audio messages.

WebSocketReal-timeReact NativeRedux
Text-to-Speech unsupported
Text Size

Soul33 needed a real-time group chat and voicemail system to support wellness communities of 200+ concurrent users. After evaluating Firebase Realtime Database, Supabase Realtime, and raw WebSocket, we chose a custom WebSocket server — lowest latency, full control over message schemas, and no vendor lock-in on message pricing at scale.

Connection lifecycle — the hard part

The trickiest part is managing the connection across app state changes. On iOS, the OS kills the socket after ~30 seconds in the background. On Android, aggressive battery optimisation on OEM ROMs (Xiaomi, Samsung) terminates connections even sooner. Our WebSocket context provider listens to AppState changes and implements exponential backoff reconnection with a maximum 30-second ceiling.

WebSocket reconnection with backoff
let backoff = 1000;
const MAX_BACKOFF = 30000;

const reconnect = async () => {
  await delay(backoff);
  socket = new WebSocket(WS_URL);
  socket.onopen = () => {
    backoff = 1000; // reset on success
    syncFromSeq(lastSeq); // replay missed messages
  };
  socket.onerror = () => {
    backoff = Math.min(backoff * 2, MAX_BACKOFF);
    reconnect();
  };
};

Message deduplication

Because we replay messages from the last known sequence number on reconnect, the client may receive duplicates. We maintain a Set of processed message IDs in Redux state, checking each incoming message before dispatching to the store. IDs are stored in a sliding window of the last 500 messages to avoid unbounded memory growth.

deduplication in Redux reducer
// In chatSlice reducer
const processedIds = new Set(state.processedIds);

if (processedIds.has(message.id)) return; // duplicate

processedIds.add(message.id);
// Sliding window — keep last 500 only
if (processedIds.size > 500) {
  const oldest = [...processedIds][0];
  processedIds.delete(oldest);
}

state.messages.push(message);
state.processedIds = [...processedIds];

Swipe-to-reply with Reanimated

Each message row is wrapped in a PanGestureHandler. A horizontal swipe beyond a 60dp threshold reveals a reply icon and stores the target message in local state as replyTarget. The reply preview bar appears above the text input. On send, the message payload includes a replyTo field with the original message ID and a 60-character preview snippet. Tapping a quoted reply scrolls the FlatList to that message using scrollToIndex and flashes a highlight animation.

Audio messages via S3

Voice messages record via react-native-audio-recorder-player, show a real-time waveform via react-native-audio-waveform, and upload to S3 with progress tracking. The S3 pre-signed URL is fetched from our backend first, then the file is PUT directly to S3 from the device — keeping our server out of the binary transfer path. The message is sent to the chat only after the S3 upload completes, including the audio duration for the playback UI.

At peak load, the system handled 200+ concurrent sessions with under 50ms average message latency measured from send to receipt. The key was optimistic UI updates — messages appear immediately client-side and are confirmed or rolled back based on the server ACK.

Share X LinkedIn
NY
Neelesh Yadav
React Native Developer with 3 yr 6 mos years building production mobile apps. Writes about performance, architecture, and mobile engineering.
Follow ↗

Comments

Leave a comment

Comments are reviewed before they appear.

More Articles

Boosting React Native Performance: New Architectures Deep Dive
8 min read
AR Try-On: Three.js + MediaPipe in React Native
10 min read
Optimizing FlatList: Reducing Re-renders by 40%
5 min read