Building a WebSocket Context in React Native with Socket.IO

ajmal_hasan

Ajmal Hasan

Posted on April 18, 2023

Building a WebSocket Context in React Native with Socket.IO

Installation ->


Before diving into WebSocket integration in a React Native application, it’s essential to consider a few things to ensure efficient, stable, and secure connections. Here’s an updated guide on using WebSocket with Socket.IO in React Native, including best practices for managing connections effectively.


Building a WebSocket Context in React Native with Socket.IO

In this guide, we’ll cover:

  1. Important considerations before using WebSockets.
  2. Setting up a WebSocket context.
  3. Best practices for using WebSockets in React Native components.

Key Considerations Before Using WebSockets

  1. Connection Stability: Mobile networks can be unstable, so ensure that your app can handle reconnections gracefully. Use reconnect options or manage reconnection manually.
  2. Authentication: Secure your WebSocket connection by sending an access token with each connection. Always validate tokens on the server side.
  3. Connection Cleanup: Always disconnect WebSocket connections when no longer needed (e.g., when a user logs out or navigates away).
  4. Error Handling: Handle connection errors, such as authentication failures, and try to refresh the token or prompt the user to log in again.
  5. Resource Management: Too many listeners or open connections can lead to memory leaks. Unsubscribe from events and close connections when they are no longer in use.

Steps 1-3: Setting Up the WebSocket Context and Provider

  1. Create WebSocket Context: WSContext will store the WebSocket connection and provide utility functions to manage it.
  2. Define WSProvider: This component initializes the WebSocket connection, manages connection state, and provides methods for interacting with the WebSocket.
  3. Create useWS Hook: The useWS hook gives access to WebSocket functions within any component wrapped by WSProvider.

Here’s how to set up these steps:


Configuration:

import { createContext, useContext, useEffect, useRef, useState } from "react";
import { io } from "socket.io-client";
import { SOCKET_URL } from "./config";
import { tokenStorage } from "@/store/storage"; // Storage utility for tokens
import { refresh_tokens } from "./apiInterceptors"; // Function to refresh tokens

// Create a WebSocket context for managing the socket instance
const WSContext = createContext(undefined);

// WebSocket Provider Component
export const WSProvider = ({ children }) => {
  // State to manage the current access token for WebSocket
  const [socketAccessToken, setSocketAccessToken] = useState(null);

  // Ref to store the WebSocket connection instance
  const socket = useRef();

  // Retrieve the token from storage on component mount
  useEffect(() => {
    const token = tokenStorage.getString("access_token");
    setSocketAccessToken(token);
  }, []);

  // Establish and manage the WebSocket connection whenever the token changes
  useEffect(() => {
    if (socketAccessToken) {
      // Disconnect existing socket before creating a new one
      if (socket.current) {
        socket.current.disconnect();
      }

      // Create a new WebSocket connection
      socket.current = io(SOCKET_URL, {
        transports: ["websocket"], // Use WebSocket transport
        withCredentials: true, // Send cookies with the request
        extraHeaders: {
          access_token: socketAccessToken || "", // Pass the token as a header
        },
      });

      // Handle connection errors (e.g., authentication issues)
      socket.current.on("connect_error", (error) => {
        if (error.message === "Authentication error") {
          console.log("Auth connection error: ", error.message);
          refresh_tokens(); // Refresh the token if authentication fails
        }
      });
    }

    // Cleanup the WebSocket connection on unmount
    return () => {
      if (socket.current) {
        socket.current.disconnect();
      }
    };
  }, [socketAccessToken]);

  // Emit an event to the server
  const emit = (event, data = {}) => {
    socket.current?.emit(event, data);
  };

  // Listen for an event from the server
  const on = (event, cb) => {
    socket.current?.on(event, cb);
  };

  // Remove a specific event listener
  const off = (event) => {
    socket.current?.off(event);
  };

  // Remove a specific listener by its name
  const removeListener = (listenerName) => {
    socket.current?.removeListener(listenerName);
  };

  // Disconnect the WebSocket connection
  const disconnect = () => {
    if (socket.current) {
      socket.current.disconnect();
      socket.current = undefined; // Clear the socket reference
    }
  };

  // Update the access token in the state
  const updateAccessToken = () => {
    const token = tokenStorage.getString("access_token");
    setSocketAccessToken(token);
  };

  // Define the WebSocket service with all utility functions
  const socketService = {
    initializeSocket: () => {}, // Placeholder for future initialization logic
    emit,
    on,
    off,
    disconnect,
    removeListener,
    updateAccessToken,
  };

  // Provide the WebSocket service to child components
  return (
    <WSContext.Provider value={socketService}>{children}</WSContext.Provider>
  );
};

// Custom hook to use the WebSocket service in components
export const useWS = () => {
  const socketService = useContext(WSContext);
  if (!socketService) {
    throw new Error("useWS must be used within a WSProvider");
  }
  return socketService;
};
Enter fullscreen mode Exit fullscreen mode

USAGE in components:

import React, { useEffect, useState } from "react";
import { WSProvider, useWS } from "./WSProvider"; // Import your WebSocket Provider and hook
import {
  View,
  Text,
  TextInput,
  Button,
  StyleSheet,
  ScrollView,
  Alert,
} from "react-native";

const WebSocketExample = () => {
  const { emit, on, off, disconnect, updateAccessToken } = useWS();
  const [messageToSend, setMessageToSend] = useState("");
  const [receivedMessage, setReceivedMessage] = useState("");

  // Example: Listening to "message" events
  useEffect(() => {
    const handleMessage = (data) => {
      console.log("Message received:", data);
      setReceivedMessage(data?.text || "No content");
    };

    // Attach listener
    on("message", handleMessage);

    // Cleanup listener on unmount
    return () => {
      off("message");
    };
  }, [on, off]);

  // Example: Emit a "chat_message" event
  const handleSendMessage = () => {
    if (messageToSend.trim()) {
      emit("chat_message", { text: messageToSend });
      console.log("Message sent:", messageToSend);
      setMessageToSend(""); // Clear input
    } else {
      Alert.alert("Error", "Message cannot be empty!");
    }
  };

  // Example: Update access token
  const handleUpdateToken = () => {
    console.log("Updating WebSocket token...");
    updateAccessToken(); // Refresh or update token
    Alert.alert("Token Updated", "The WebSocket token has been updated.");
  };

  // Example: Disconnect WebSocket
  const handleDisconnect = () => {
    console.log("Disconnecting WebSocket...");
    disconnect();
    Alert.alert("Disconnected", "The WebSocket connection has been closed.");
  };

  return (
    <ScrollView contentContainerStyle={styles.container}>
      <Text style={styles.title}>WebSocket Example</Text>

      {/* Send Message */}
      <View style={styles.section}>
        <Text style={styles.subtitle}>Send a Message</Text>
        <TextInput
          value={messageToSend}
          onChangeText={setMessageToSend}
          placeholder="Type your message..."
          style={styles.input}
        />
        <Button title="Send Message" onPress={handleSendMessage} />
      </View>

      {/* Received Message */}
      <View style={styles.section}>
        <Text style={styles.subtitle}>Received Message</Text>
        <Text style={styles.message}>
          {receivedMessage || "No messages yet."}
        </Text>
      </View>

      {/* Update Token */}
      <View style={styles.section}>
        <Text style={styles.subtitle}>Update Access Token</Text>
        <Button title="Update Token" onPress={handleUpdateToken} />
      </View>

      {/* Disconnect WebSocket */}
      <View style={styles.section}>
        <Text style={styles.subtitle}>Disconnect WebSocket</Text>
        <Button title="Disconnect" onPress={handleDisconnect} />
      </View>
    </ScrollView>
  );
};

// Wrap the app with the WebSocket provider
const App = () => {
  return (
    <WSProvider>
      <WebSocketExample />
    </WSProvider>
  );
};

export default App;

// Styles
const styles = StyleSheet.create({
  container: {
    flexGrow: 1,
    padding: 20,
    backgroundColor: "#f9f9f9",
  },
  title: {
    fontSize: 24,
    fontWeight: "bold",
    textAlign: "center",
    marginBottom: 20,
  },
  section: {
    marginBottom: 20,
  },
  subtitle: {
    fontSize: 18,
    fontWeight: "bold",
    marginBottom: 10,
  },
  input: {
    borderWidth: 1,
    borderColor: "#ccc",
    borderRadius: 5,
    padding: 10,
    marginBottom: 10,
  },
  message: {
    fontSize: 16,
    color: "#333",
  },
});

Enter fullscreen mode Exit fullscreen mode

Summary

By following these practices, you can ensure that your WebSocket connections remain stable, secure, and efficient:

  • Set up and manage WebSocket connections using a centralized context (WSProvider).
  • Implement proper connection management (e.g., reconnection, cleanup).
  • Handle authentication with tokens and error handling for a secure connection.
  • Use WebSocket effectively in components by managing listeners and cleaning up to avoid memory leaks.

This setup is ideal for a real-time React Native application, offering an organized, scalable solution for WebSocket communication.

💖 💪 🙅 🚩
ajmal_hasan
Ajmal Hasan

Posted on April 18, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related