WebSockets in React Native: Real-Time Communication and Multiplayer Apps

13 min read
#React Native#WebSockets#Real-Time#Socket.IO#Networking

Real-time communication separates basic apps from engaging experiences. Whether building live chat, collaborative editing, multiplayer games, or live notifications, WebSockets enable instant bidirectional communication between client and server. React Native, combined with Socket.IO, provides a robust framework for real-time features at scale.

Understanding WebSockets vs HTTP

Traditional HTTP is request-response: client asks, server answers, connection closes. This model breaks down for real-time scenarios.

HTTP Request/Response:
Client: "Hey server, do you have new messages?"
Server: "No messages"
[Connection closes]
[5 seconds later...]
Client: "Hey server, do you have new messages?"
Server: "Yes! 3 new messages"

WebSocket (persistent connection):
Client ↔ Server (connection stays open)
Server: "You have a new message!" (instant, no polling)
Server: "Chat from John: Hello!" (instant)
Client: "I'm typing..." (sent immediately)

WebSocket advantages:

  • Persistent two-way connection
  • Lower latency (milliseconds, not seconds)
  • Server can initiate messages
  • Reduced bandwidth (no HTTP headers on each message)
  • Perfect for real-time features

Key Takeaways

  • WebSockets enable bidirectional communication with millisecond latency
  • Socket.IO abstracts WebSocket complexity with automatic fallbacks and reconnection
  • Connection management requires explicit handling of connect/disconnect/error states
  • Message queuing preserves messages sent during disconnections
  • Network awareness keeps users informed of connection state
  • Scaling considerations include room/namespace organization and server load distribution

Setting Up Socket.IO in React Native

Socket.IO is a library built on WebSockets that handles fallbacks, reconnection, and event management. It's the industry standard for real-time React Native apps.

Installation

npm install socket.io-client

Creating a Socket Service

import io from 'socket.io-client';

class SocketService {
  constructor(serverUrl) {
    this.serverUrl = serverUrl;
    this.socket = null;
    this.listeners = {};
  }

  connect() {
    this.socket = io(this.serverUrl, {
      reconnection: true,
      reconnectionDelay: 1000,
      reconnectionDelayMax: 5000,
      reconnectionAttempts: 5,
      transports: ['websocket'],
    });

    this.socket.on('connect', () => {
      console.log('Socket connected:', this.socket.id);
      this.emit('userOnline', {userId: this.userId});
    });

    this.socket.on('disconnect', (reason) => {
      console.log('Socket disconnected:', reason);
    });

    this.socket.on('connect_error', (error) => {
      console.error('Connection error:', error);
    });
  }

  disconnect() {
    if (this.socket) {
      this.socket.disconnect();
    }
  }

  emit(event, data) {
    if (this.socket) {
      this.socket.emit(event, data);
    }
  }

  on(event, callback) {
    if (this.socket) {
      this.socket.on(event, callback);
      this.listeners[event] = callback;
    }
  }

  off(event) {
    if (this.socket && this.listeners[event]) {
      this.socket.off(event, this.listeners[event]);
      delete this.listeners[event];
    }
  }
}

export default SocketService;

Initializing Socket in Your App

import {useEffect} from 'react';
import SocketService from './services/SocketService';

const socketService = new SocketService('https://api.myapp.com');

export const useSocket = () => {
  useEffect(() => {
    socketService.connect();

    return () => {
      socketService.disconnect();
    };
  }, []);

  return socketService;
};

Real-Time Chat Implementation

Building a live chat feature demonstrates core WebSocket patterns.

Chat Screen Component

import React, {useState, useEffect, useRef} from 'react';
import {
  View,
  FlatList,
  TextInput,
  TouchableOpacity,
  Text,
  KeyboardAvoidingView,
} from 'react-native';
import {useSocket} from '../hooks/useSocket';

const ChatScreen = ({conversationId, userId}) => {
  const [messages, setMessages] = useState([]);
  const [inputText, setInputText] = useState('');
  const [isTyping, setIsTyping] = useState(false);
  const socketService = useSocket();
  const typingTimeout = useRef(null);

  useEffect(() => {
    socketService.on('messageReceived', (message) => {
      setMessages((prev) => [...prev, message]);
    });

    socketService.on('userTyping', (data) => {
      if (data.userId !== userId) {
        setIsTyping(true);
        setTimeout(() => setIsTyping(false), 2000);
      }
    });

    return () => {
      socketService.off('messageReceived');
      socketService.off('userTyping');
    };
  }, [userId]);

  const handleSendMessage = () => {
    if (inputText.trim()) {
      const message = {
        id: Date.now(),
        text: inputText,
        userId,
        timestamp: new Date(),
        conversationId,
      };

      socketService.emit('sendMessage', message);
      setInputText('');
    }
  };

  const handleTyping = (text) => {
    setInputText(text);

    socketService.emit('userTyping', {
      conversationId,
      userId,
      isTyping: true,
    });

    clearTimeout(typingTimeout.current);
    typingTimeout.current = setTimeout(() => {
      socketService.emit('userTyping', {
        conversationId,
        userId,
        isTyping: false,
      });
    }, 1000);
  };

  return (
    <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
      <FlatList
        data={messages}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({item}) => (
          <View
            style={{
              alignSelf: item.userId === userId ? 'flex-end' : 'flex-start',
              backgroundColor:
                item.userId === userId ? '#06B6D4' : '#E5E7EB',
              padding: 10,
              borderRadius: 8,
              marginVertical: 4,
              marginHorizontal: 10,
              maxWidth: '80%',
            }}
          >
            <Text>{item.text}</Text>
          </View>
        )}
      />

      {isTyping && <Text style={{padding: 10}}>User is typing...</Text>}

      <View style={{flexDirection: 'row', padding: 10}}>
        <TextInput
          style={{flex: 1, borderWidth: 1, borderRadius: 20, paddingHorizontal: 15}}
          placeholder="Type a message..."
          value={inputText}
          onChangeText={handleTyping}
        />
        <TouchableOpacity
          onPress={handleSendMessage}
          style={{marginLeft: 10, justifyContent: 'center'}}
        >
          <Text style={{color: '#06B6D4', fontWeight: '600'}}>Send</Text>
        </TouchableOpacity>
      </View>
    </KeyboardAvoidingView>
  );
};

export default ChatScreen;

Connection Management and Reconnection

WebSocket connections can drop. Robust apps handle disconnections gracefully.

class ReliableSocketService extends SocketService {
  constructor(serverUrl, userId) {
    super(serverUrl);
    this.userId = userId;
    this.messageQueue = [];
    this.isConnected = false;
  }

  connect() {
    super.connect();

    this.socket.on('connect', () => {
      this.isConnected = true;
      console.log('Connected');

      this.flushMessageQueue();
      this.socket.emit('userOnline', {userId: this.userId});
    });

    this.socket.on('disconnect', (reason) => {
      this.isConnected = false;
      console.log('Disconnected:', reason);

      if (reason === 'io server disconnect') {
        this.socket.connect();
      }
    });

    this.socket.on('connect_error', (error) => {
      console.error('Connection error:', error);
      this.isConnected = false;
    });
  }

  emit(event, data) {
    if (this.isConnected && this.socket) {
      this.socket.emit(event, data);
    } else {
      this.messageQueue.push({event, data, timestamp: Date.now()});
    }
  }

  flushMessageQueue() {
    while (this.messageQueue.length > 0) {
      const {event, data} = this.messageQueue.shift();
      if (this.socket) {
        this.socket.emit(event, data);
      }
    }
  }

  getConnectionStatus() {
    return {
      isConnected: this.isConnected,
      queuedMessages: this.messageQueue.length,
    };
  }
}

Event-Based Communication Patterns

WebSockets work with events. Design your event structure carefully.

Structured Event Naming

const EVENTS = {
  USER: {
    ONLINE: 'user:online',
    OFFLINE: 'user:offline',
    TYPING: 'user:typing',
    STATUS_CHANGED: 'user:status:changed',
  },
  MESSAGE: {
    SENT: 'message:sent',
    RECEIVED: 'message:received',
    EDITED: 'message:edited',
    DELETED: 'message:deleted',
  },
  NOTIFICATION: {
    NEW: 'notification:new',
    READ: 'notification:read',
  },
};

socketService.emit(EVENTS.MESSAGE.SENT, messageData);
socketService.on(EVENTS.MESSAGE.RECEIVED, handleNewMessage);

Request-Response Pattern

Sometimes you need a response to a sent event.

const sendWithResponse = (event, data, timeout = 5000) => {
  return new Promise((resolve, reject) => {
    const responseEvent = `${event}:response`;
    const timer = setTimeout(() => {
      socketService.off(responseEvent);
      reject(new Error(`No response for ${event}`));
    }, timeout);

    socketService.on(responseEvent, (response) => {
      clearTimeout(timer);
      socketService.off(responseEvent);
      resolve(response);
    });

    socketService.emit(event, data);
  });
};

const userProfile = await sendWithResponse('user:getProfile', {userId: 123});

Handling Network State

Mobile apps face intermittent connections. Integrate WebSocket logic with network state.

import NetInfo from '@react-native-community/netinfo';

class NetworkAwareSocket extends ReliableSocketService {
  constructor(serverUrl, userId) {
    super(serverUrl, userId);
    this.networkState = null;
  }

  async initialize() {
    this.unsubscribeNetInfo = NetInfo.addEventListener((state) => {
      this.networkState = state;

      if (state.isConnected && !this.isConnected) {
        console.log('Network restored, reconnecting...');
        this.connect();
      } else if (!state.isConnected && this.isConnected) {
        console.log('Network lost, will reconnect when available');
        this.disconnect();
      }
    });

    this.connect();
  }

  cleanup() {
    if (this.unsubscribeNetInfo) {
      this.unsubscribeNetInfo();
    }
    this.disconnect();
  }
}

Scaling WebSocket Architecture

Production apps need proper server-side architecture.

Server-Side Considerations

const http = require('http');
const socketIo = require('socket.io');
const redis = require('redis');

const server = http.createServer();
const io = socketIo(server, {
  cors: {origin: '*'},
  transports: ['websocket'],
});

const redisClient = redis.createClient();

io.on('connection', (socket) => {
  socket.on('message:send', (data) => {
    io.to(data.conversationId).emit('message:received', data);

    redisClient.lpush(`conversation:${data.conversationId}`, JSON.stringify(data));
  });

  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

server.listen(3000);

Rooms and Namespaces

Organize connections logically:

socketService.socket.emit('joinConversation', {conversationId: 123});

socketService.on('conversationMessage', (message) => {
  console.log('Message in conversation:', message);
});

socketService.socket.emit('leaveConversation', {conversationId: 123});

Performance Optimization

Real-time apps can overwhelm with message volume.

class OptimizedSocketService extends NetworkAwareSocket {
  constructor(serverUrl, userId) {
    super(serverUrl, userId);
    this.messageBuffer = [];
    this.bufferInterval = 100;
    this.lastFlush = Date.now();
  }

  emitBatched(event, data) {
    this.messageBuffer.push({event, data});

    const timeSinceFlush = Date.now() - this.lastFlush;
    if (this.messageBuffer.length >= 10 || timeSinceFlush > this.bufferInterval) {
      this.flushBuffer();
    }
  }

  flushBuffer() {
    if (this.messageBuffer.length === 0) return;

    this.emit('batch', this.messageBuffer);
    this.messageBuffer = [];
    this.lastFlush = Date.now();
  }
}

Best Practices Summary

Connection Management:

  • Establish connections in app lifecycle (not per screen)
  • Clean up listeners when components unmount
  • Handle reconnection automatically
  • Queue messages during disconnection

Event Design:

  • Use consistent event naming conventions
  • Include timestamps in all messages
  • Implement request-response patterns for critical operations
  • Validate data on both client and server

Network Awareness:

  • Monitor network connectivity
  • Gracefully handle intermittent connections
  • Provide user feedback on connection status
  • Sync state when reconnecting

Performance:

  • Batch frequent events
  • Debounce typing indicators
  • Limit message frequency (rate limiting)
  • Monitor memory usage with many listeners

Security:

  • Validate user permissions server-side
  • Use authentication tokens
  • Implement rate limiting
  • Sanitize all incoming data

WebSockets transform React Native apps from polling-based to truly real-time. Combined with proper connection management, event design, and network awareness, you can build responsive, scalable multiplayer experiences that keep users engaged.


Let's bring your app idea to life

I specialize in mobile and backend development.

Share this article

Related Articles