DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 31: Real-Time Applications with React

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 with React

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

  1. Connection Management: Always clean up connections in useEffect cleanup functions
  2. Error Handling: Implement robust error handling and reconnection logic
  3. Performance: Use throttling and batching for high-frequency updates
  4. User Experience: Provide clear connection status indicators
  5. Security: Validate all incoming messages and implement proper authentication
  6. Fallback Options: Provide polling as a fallback when WebSockets aren’t available
  7. Message Ordering: Implement message sequencing for critical applications
  8. 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.

Related Articles

Scroll to Top