Real-time applications provide instant updates to users without requiring manual page refreshes. In React, we can build these applications using various technologies like WebSockets, Server-Sent Events (SSE), and polling techniques. This chapter explores different approaches to creating real-time applications with React.
What are Real-Time Applications?
Real-time applications are software systems that respond to user inputs or external events within a very short time frame, typically milliseconds or seconds. Examples include:
- Chat applications
- Live notifications
- Collaborative editing tools
- Real-time dashboards
- Live sports scores
- Stock trading platforms
- Gaming applications
WebSockets in React
WebSockets provide full-duplex communication between client and server, making them ideal for real-time applications.
Basic WebSocket Implementation
import React, { useState, useEffect, useRef } from 'react';
const WebSocketChat = () => {
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState('');
const [connectionStatus, setConnectionStatus] = useState('Connecting...');
const ws = useRef(null);
useEffect(() => {
// Create WebSocket connection
ws.current = new WebSocket('ws://localhost:8080');
ws.current.onopen = () => {
setConnectionStatus('Connected');
console.log('WebSocket Connected');
};
ws.current.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
ws.current.onclose = () => {
setConnectionStatus('Disconnected');
console.log('WebSocket Disconnected');
};
ws.current.onerror = (error) => {
console.error('WebSocket Error:', error);
setConnectionStatus('Error');
};
// Cleanup on component unmount
return () => {
ws.current.close();
};
}, []);
const sendMessage = () => {
if (inputMessage.trim() && ws.current.readyState === WebSocket.OPEN) {
const message = {
id: Date.now(),
text: inputMessage,
timestamp: new Date().toISOString(),
user: 'User'
};
ws.current.send(JSON.stringify(message));
setInputMessage('');
}
};
return (
<div className="chat-container">
<div className="connection-status">
Status: {connectionStatus}
</div>
<div className="messages">
{messages.map(message => (
<div key={message.id} className="message">
<strong>{message.user}:</strong> {message.text}
<span className="timestamp">
{new Date(message.timestamp).toLocaleTimeString()}
</span>
</div>
))}
</div>
<div className="input-area">
<input
type="text"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Type a message..."
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
};
export default WebSocketChat;
Custom WebSocket Hook
Creating a reusable hook for WebSocket connections:
import { useState, useEffect, useRef, useCallback } from 'react';
const useWebSocket = (url) => {
 const [socket, setSocket] = useState(null);
 const [lastMessage, setLastMessage] = useState(null);
 const [readyState, setReadyState] = useState(0);
 const [connectionStatus, setConnectionStatus] = useState('Connecting');
 const ws = useRef();
 useEffect(() => {
  ws.current = new WebSocket(url);
 Â
  ws.current.onopen = () => {
   setReadyState(ws.current.readyState);
   setConnectionStatus('Open');
   setSocket(ws.current);
  };
  ws.current.onclose = () => {
   setReadyState(ws.current.readyState);
   setConnectionStatus('Closed');
  };
  ws.current.onerror = () => {
   setReadyState(ws.current.readyState);
   setConnectionStatus('Error');
  };
  ws.current.onmessage = (event) => {
   setLastMessage(event.data);
  };
  return () => {
   ws.current.close();
  };
 }, [url]);
 const sendMessage = useCallback((message) => {
  if (ws.current && ws.current.readyState === WebSocket.OPEN) {
   ws.current.send(message);
  }
 }, []);
 return {
  sendMessage,
  lastMessage,
  readyState,
  connectionStatus
 };
};
// Usage
const ChatApp = () => {
 const { sendMessage, lastMessage, connectionStatus } = useWebSocket('ws://localhost:8080');
 const [messages, setMessages] = useState([]);
 useEffect(() => {
  if (lastMessage) {
   setMessages(prev => [...prev, JSON.parse(lastMessage)]);
  }
 }, [lastMessage]);
 return (
  <div>
   <p>Connection: {connectionStatus}</p>
   {/* Rest of component */}
  </div>
 );
};
Server-Sent Events (SSE)
SSE provides a simpler alternative to WebSockets for one-way communication from server to client.
Basic SSE Implementation
import React, { useState, useEffect } from 'react';
const ServerSentEventsComponent = () => {
const [notifications, setNotifications] = useState([]);
const [connectionStatus, setConnectionStatus] = useState('Connecting');
useEffect(() => {
const eventSource = new EventSource('/api/events');
eventSource.onopen = () => {
setConnectionStatus('Connected');
};
eventSource.onmessage = (event) => {
const notification = JSON.parse(event.data);
setNotifications(prev => [...prev, notification]);
};
eventSource.onerror = () => {
setConnectionStatus('Error');
};
return () => {
eventSource.close();
};
}, []);
return (
<div>
<h3>Real-time Notifications</h3>
<p>Status: {connectionStatus}</p>
<div className="notifications">
{notifications.map(notification => (
<div key={notification.id} className="notification">
{notification.message}
<small>{new Date(notification.timestamp).toLocaleString()}</small>
</div>
))}
</div>
</div>
);
};
Custom SSE Hook
import { useState, useEffect } from 'react';
const useServerSentEvents = (url) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [connectionState, setConnectionState] = useState('CONNECTING');
useEffect(() => {
const eventSource = new EventSource(url);
eventSource.onopen = () => {
setConnectionState('OPEN');
setError(null);
};
eventSource.onmessage = (event) => {
setData(JSON.parse(event.data));
};
eventSource.onerror = (err) => {
setConnectionState('CLOSED');
setError(err);
};
return () => {
eventSource.close();
};
}, [url]);
return { data, error, connectionState };
};

Polling Techniques
Polling involves making regular HTTP requests to check for updates.
Simple Polling
import React, { useState, useEffect } from 'react';
const PollingComponent = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Polling error:', error);
} finally {
setLoading(false);
}
};
// Initial fetch
fetchData();
// Set up polling interval
const interval = setInterval(fetchData, 5000); // Poll every 5 seconds
return () => clearInterval(interval);
}, []);
return (
<div>
{loading && <p>Loading...</p>}
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
Long Polling
import React, { useState, useEffect, useCallback } from 'react';
const LongPollingComponent = () => {
const [messages, setMessages] = useState([]);
const [isPolling, setIsPolling] = useState(false);
const longPoll = useCallback(async () => {
setIsPolling(true);
try {
const response = await fetch('/api/long-poll', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache',
},
});
if (response.ok) {
const newMessages = await response.json();
setMessages(prev => [...prev, ...newMessages]);
}
} catch (error) {
console.error('Long polling error:', error);
} finally {
setIsPolling(false);
}
}, []);
useEffect(() => {
longPoll();
}, [longPoll]);
// Continue polling when previous request completes
useEffect(() => {
if (!isPolling) {
const timeout = setTimeout(longPoll, 1000);
return () => clearTimeout(timeout);
}
}, [isPolling, longPoll]);
return (
<div>
<h3>Long Polling Messages</h3>
{messages.map(message => (
<div key={message.id}>{message.content}</div>
))}
</div>
);
};
Real-Time Dashboard Example
Here’s a comprehensive example of a real-time dashboard:
import React, { useState, useEffect } from 'react';
const RealTimeDashboard = () => {
const [metrics, setMetrics] = useState({
users: 0,
sales: 0,
orders: 0,
revenue: 0
});
const [activities, setActivities] = useState([]);
const [notifications, setNotifications] = useState([]);
useEffect(() => {
const ws = new WebSocket('ws://localhost:8080/dashboard');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'METRICS_UPDATE':
setMetrics(data.metrics);
break;
case 'NEW_ACTIVITY':
setActivities(prev => [data.activity, ...prev.slice(0, 9)]);
break;
case 'NOTIFICATION':
setNotifications(prev => [data.notification, ...prev]);
break;
default:
break;
}
};
return () => ws.close();
}, []);
const dismissNotification = (id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};
return (
<div className="dashboard">
<h1>Real-Time Dashboard</h1>
{/* Metrics Cards */}
<div className="metrics-grid">
<div className="metric-card">
<h3>Active Users</h3>
<p className="metric-value">{metrics.users}</p>
</div>
<div className="metric-card">
<h3>Sales Today</h3>
<p className="metric-value">{metrics.sales}</p>
</div>
<div className="metric-card">
<h3>Orders</h3>
<p className="metric-value">{metrics.orders}</p>
</div>
<div className="metric-card">
<h3>Revenue</h3>
<p className="metric-value">${metrics.revenue}</p>
</div>
</div>
{/* Recent Activities */}
<div className="activities-section">
<h2>Recent Activities</h2>
<div className="activities-list">
{activities.map(activity => (
<div key={activity.id} className="activity-item">
<span className="activity-time">
{new Date(activity.timestamp).toLocaleTimeString()}
</span>
<span className="activity-description">
{activity.description}
</span>
</div>
))}
</div>
</div>
{/* Notifications */}
{notifications.length > 0 && (
<div className="notifications">
{notifications.map(notification => (
<div key={notification.id} className="notification-item">
<span>{notification.message}</span>
<button onClick={() => dismissNotification(notification.id)}>
×
</button>
</div>
))}
</div>
)}
</div>
);
};
Error Handling and Reconnection
Implementing robust error handling and automatic reconnection:
import React, { useState, useEffect, useRef } from 'react';
const ReliableWebSocket = ({ url, onMessage }) => {
const [connectionStatus, setConnectionStatus] = useState('Connecting');
const [reconnectAttempts, setReconnectAttempts] = useState(0);
const ws = useRef(null);
const reconnectTimeoutRef = useRef(null);
const maxReconnectAttempts = 5;
const reconnectInterval = 3000;
const connect = () => {
ws.current = new WebSocket(url);
ws.current.onopen = () => {
setConnectionStatus('Connected');
setReconnectAttempts(0);
};
ws.current.onmessage = (event) => {
onMessage(event.data);
};
ws.current.onclose = () => {
setConnectionStatus('Disconnected');
attemptReconnect();
};
ws.current.onerror = () => {
setConnectionStatus('Error');
};
};
const attemptReconnect = () => {
if (reconnectAttempts < maxReconnectAttempts) {
setConnectionStatus('Reconnecting...');
setReconnectAttempts(prev => prev + 1);
reconnectTimeoutRef.current = setTimeout(() => {
connect();
}, reconnectInterval);
} else {
setConnectionStatus('Failed to reconnect');
}
};
useEffect(() => {
connect();
return () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (ws.current) {
ws.current.close();
}
};
}, [url]);
const sendMessage = (message) => {
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(message);
return true;
}
return false;
};
return {
sendMessage,
connectionStatus,
reconnectAttempts
};
};

Performance Considerations
Message Throttling
import { useState, useEffect, useRef } from 'react';
const useThrottledMessages = (messages, delay = 100) => {
const [throttledMessages, setThrottledMessages] = useState([]);
const timeoutRef = useRef(null);
useEffect(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setThrottledMessages(messages);
}, delay);
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [messages, delay]);
return throttledMessages;
};
Message Batching
const useBatchedMessages = (batchSize = 10, batchDelay = 1000) => {
 const [messages, setMessages] = useState([]);
 const [displayMessages, setDisplayMessages] = useState([]);
 const batchRef = useRef([]);
 const timeoutRef = useRef(null);
 const addMessage = (message) => {
  batchRef.current.push(message);
  if (batchRef.current.length >= batchSize) {
   processBatch();
  } else if (!timeoutRef.current) {
   timeoutRef.current = setTimeout(processBatch, batchDelay);
  }
 };
 const processBatch = () => {
  if (batchRef.current.length > 0) {
   setDisplayMessages(prev => [...prev, ...batchRef.current]);
   batchRef.current = [];
  }
 Â
  if (timeoutRef.current) {
   clearTimeout(timeoutRef.current);
   timeoutRef.current = null;
  }
 };
 return { displayMessages, addMessage };
};
Testing Real-Time Features
Mocking WebSocket for Tests
// __mocks__/websocket.js
class MockWebSocket {
constructor(url) {
this.url = url;
this.readyState = WebSocket.CONNECTING;
setTimeout(() => {
this.readyState = WebSocket.OPEN;
if (this.onopen) this.onopen();
}, 100);
}
send(data) {
// Simulate sending data
console.log('Sending:', data);
}
close() {
this.readyState = WebSocket.CLOSED;
if (this.onclose) this.onclose();
}
// Method to simulate receiving messages in tests
simulateMessage(data) {
if (this.onmessage) {
this.onmessage({ data });
}
}
}
global.WebSocket = MockWebSocket;
Testing Component with Real-Time Features
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import WebSocketChat from './WebSocketChat';
// Mock WebSocket
jest.mock('./websocket');
test('displays connection status', async () => {
render(<WebSocketChat />);
expect(screen.getByText('Connecting...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Connected')).toBeInTheDocument();
});
});
test('displays received messages', async () => {
const { container } = render(<WebSocketChat />);
// Simulate receiving a message
const mockMessage = {
id: 1,
text: 'Hello World',
user: 'TestUser',
timestamp: new Date().toISOString()
};
// You would need to expose the WebSocket instance or use a testing library
// that can simulate WebSocket messages
});

Best Practices
- Connection Management: Always clean up connections in useEffect cleanup functions
- Error Handling: Implement robust error handling and reconnection logic
- Performance: Use throttling and batching for high-frequency updates
- User Experience: Provide clear connection status indicators
- Security: Validate all incoming messages and implement proper authentication
- Fallback Options: Provide polling as a fallback when WebSockets aren’t available
- Message Ordering: Implement message sequencing for critical applications
- Resource Management: Limit the number of stored messages to prevent memory leaks
Summary
Real-time applications in React can be built using various approaches:
- WebSockets for bidirectional communication
- Server-Sent Events for server-to-client updates
- Polling for simple periodic updates
- Long Polling for more efficient server-push simulation
Choose the right approach based on your specific requirements for latency, server resources, and browser compatibility. Always implement proper error handling, reconnection logic, and performance optimizations for production applications.
The key to successful real-time applications is balancing real-time functionality with performance, user experience, and resource management. Start with simple implementations and gradually add complexity as needed.