
Handling WebSockets in React Native with Socket.IO: A Complete Guide
- What is socket.io?#what-is-socketio
- 1. Setting Up the Project#1-setting-up-the-project
- 2. Creating the Socket Context#2-creating-the-socket-context
- 3. Handling Acknowledgements and Event Queue#3-handling-acknowledgements-and-event-queue
- 4. Integrating into the App#4-integrating-into-the-app
- Conclusion#conclusion
WebSockets are essential for creating interactive, real-time apps, but using them in a mobile setting like React Native presents difficulties due to poor internet, disconnected connections, and retries.
What is socket.io?
Socket.IO is a library that enables low-latency, bidirectional and event-based communication between a client and a server. Socket.IO manages events, retries, and connection management, making WebSocket implementation easier. It is an great option for guaranteeing good event delivery as it also supports acknowledgements.
1. Setting Up the Project
Install Dependencies
First, create a React Native project and install the required dependencies:
npx @react-native-community/cli@latest init SocketIOExample
cd SocketIOExample
npm install socket.io-client
2. Creating the Socket Context
To make the socket instance globally accessible, we’ll use a React Context.
SocketContext.tsx
import React, {createContext, useContext, useEffect, useRef} from 'react';
import {AppState} from 'react-native';
import {io, Socket} from 'socket.io-client';
// Define the context type
interface Message {
id: string;
text: string;
}
interface SocketContextType {
socket: Socket | null;
isConnected: boolean;
messages: Message[];
setMessage: React.Dispatch<React.SetStateAction<Message[]>>;
}
const SocketContext = createContext<SocketContextType | undefined>(undefined);
export const SocketProvider: React.FC<{children: React.ReactNode}> = ({
children,
}) => {
const socketRef = useRef<Socket | null>(null);
const [isConnected, setIsConnected] = React.useState(false);
const [message, setMessage] = React.useState<Message[]>([]);
useEffect(() => {
socketRef.current = io('http://localhost:3000', {
reconnection: true, // Enable automatic reconnection
reconnectionAttempts: Infinity, // Number of reconnection attempts
reconnectionDelay: 1000, // Initial delay between reconnection attempts (ms)
reconnectionDelayMax: 10000, // Maximum delay between reconnection attempts (ms)
timeout: 10000, // Connection timeout (ms)
autoConnect: true, // Automatically connect to the server when the component mounts
forceNew: false, // Do not force a new connection on each connection attempt
});
const socket = socketRef.current;
const handleAppStateChange = (nextAppState: string) => {
if (nextAppState === 'active') {
socket?.connect();
} else if (nextAppState === 'background') {
socket?.disconnect();
}
};
const appStateSubscription = AppState.addEventListener(
'change',
handleAppStateChange,
);
socket.on('connect', () => {
console.log('Connected to server');
setIsConnected(true);
});
socket.on('disconnect', reason => {
console.warn('Disconnected:', reason);
setIsConnected(false);
});
socket.on('connect_error', error => {
console.error('Connection error:', error.message);
});
socket.on('message', data => {
if (data && data.id && data.text) {
setMessage(prevValue => [...prevValue, data]);
} else {
console.error('Invalid message data:', data);
}
});
return () => {
socket.disconnect();
socket.removeAllListeners();
appStateSubscription.remove();
};
}, []);
return (
<SocketContext.Provider
value={{
socket: socketRef.current,
isConnected,
messages: message,
setMessage,
}}>
{children}
</SocketContext.Provider>
);
};
export const useSocket = (): SocketContextType => {
const context = useContext(SocketContext);
if (!context) {
throw new Error('useSocket must be used within a SocketProvider');
}
return context;
};
Key Features:
- Automatically manages connection and disconnection.
- Provides a global socket instance.
3. Handling Acknowledgements and Event Queue
Acknowledgements ensure the server processes your events. We’ll also handle retries and queue events when disconnected.
useSocketInstance.tsx
import {useCallback, useEffect, useState} from 'react';
import {useSocket} from './SocketContext';
interface EventQueueItem {
eventName: string;
eventData: any;
retryCount: number;
}
export const useSocketInstance = () => {
const {socket, isConnected, messages: message, setMessage} = useSocket();
const [eventQueue, setEventQueue] = useState<EventQueueItem[]>([]);
const MAX_RETRIES = 3;
const ACK_TIMEOUT = 5000; // Timeout in ms
const addToQueue = (eventName: string, eventData: any, retryCount = 0) => {
setEventQueue(prev => [...prev, {eventName, eventData, retryCount}]);
};
const emitWithAck = useCallback(
async (
eventName: string,
eventData: any,
retryCount = 0,
): Promise<void> => {
if (!socket || !isConnected) {
console.warn(`Socket disconnected. Queuing event: ${eventName}`);
addToQueue(eventName, eventData, retryCount);
return;
}
try {
const response = await socket
.timeout(ACK_TIMEOUT)
.emitWithAck(eventName, eventData);
if (response.status === 'ok') {
console.log(`Event ${eventName} delivered successfully.`);
} else {
console.error(`Server error for event ${eventName}:`, response.error);
throw new Error(response.error);
}
} catch (error) {
console.warn(`Ack failed for event ${eventName}:`, error);
if (retryCount < MAX_RETRIES) {
console.log(
`Retrying event ${eventName} (${retryCount + 1}/${MAX_RETRIES})`,
);
addToQueue(eventName, eventData, retryCount + 1);
} else {
console.error(
`Event ${eventName} failed after ${MAX_RETRIES} retries.`,
);
// Notify the caller about the failure
throw new Error(
`Event ${eventName} failed after ${MAX_RETRIES} retries.`,
);
}
}
},
[isConnected, socket],
);
const processQueue = useCallback(() => {
if (socket && isConnected) {
eventQueue.forEach(({eventName, eventData, retryCount}) => {
emitWithAck(eventName, eventData, retryCount);
});
setEventQueue([]); // Clear the queue after processing
}
}, [emitWithAck, eventQueue, isConnected, socket]);
useEffect(() => {
if (isConnected) {
processQueue();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isConnected, eventQueue]); // This will rerun the effect when either the socket is connected and when there is queue to run
return {emitWithAck, isConnected, message, setMessage};
};
4. Integrating into the App
App.tsx
import React from 'react';
import {SocketProvider} from './SocketContext';
import ChatScreen from './ChatScreen';
function App(): React.JSX.Element {
return (
<SocketProvider>
<ChatScreen />
</SocketProvider>
);
}
export default App;
ChatScreen.tsx
import React, {useState} from 'react';
import {View, TextInput, Button, StyleSheet, Alert, Text} from 'react-native';
import {useSocketInstance} from './useSocketInstance';
const ChatScreen = () => {
const {emitWithAck, isConnected, message} = useSocketInstance();
const [currentMessage, setCurrentMessage] = useState('');
const sendMessage = () => {
if (currentMessage.trim()) {
emitWithAck('sendMessage', {currentMessage}).catch(error => {
console.error('Error sending message:', error);
// Handle the error, e.g., display an error message to the user
Alert.alert('Error sending message: ' + error.message);
});
setCurrentMessage('');
}
};
return (
<View style={styles.container}>
{message.map(value => (
<Text key={value.id}>{value.text}</Text>
))}
<TextInput
value={currentMessage}
onChangeText={setCurrentMessage}
placeholder="Type a message"
style={styles.input}
/>
<Button
title="Send Message"
onPress={sendMessage}
disabled={!isConnected}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {padding: 20},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 5,
marginBottom: 10,
padding: 10,
},
});
export default ChatScreen;
Conclusion
This setup ensures:
- Reliable Event Delivery: Acknowledgements with retries and timeouts ensure messages are not lost.
- Reconnection Management: Automatically processes queued events when the connection is restored.
- Global Access: A centralized socket context simplifies application logic.
- With this robust approach, your React Native app can handle WebSocket communication even under unreliable network conditions.
Github Link: https://github.com/dubbyding/react-native-socket