featured image

Real-Time Chat Application with WebSockets

A full-stack real-time chat application built with React, Node.js, and Socket.io featuring rooms, typing indicators, and message history.

Published

Fri Nov 01 2024

Technologies Used

React Node.js Socket.io WebSockets Express MongoDB TypeScript
View on GitHub

Live Demo

Loading demo...

Project Overview

This real-time chat application demonstrates modern web communication patterns using WebSockets. The project showcases full-stack development skills, real-time data synchronization, and scalable architecture design.

Key Features

  • Real-time messaging with instant delivery
  • Multiple chat rooms with dynamic creation
  • Typing indicators to show when users are composing messages
  • User presence tracking (online/offline status)
  • Message history with MongoDB persistence
  • User authentication with JWT tokens
  • Responsive design that works on mobile and desktop

Technical Implementation

Backend Architecture

The backend is built with Node.js and Express, using Socket.io for WebSocket management:

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

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
  cors: {
    origin: process.env.CLIENT_URL,
    methods: ['GET', 'POST']
  }
});

// Socket.io connection handling
io.on('connection', (socket) => {
  console.log('New client connected:', socket.id);
  
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    socket.to(roomId).emit('user-joined', socket.id);
  });
  
  socket.on('send-message', async (data) => {
    const message = await Message.create({
      content: data.content,
      userId: data.userId,
      roomId: data.roomId,
      timestamp: new Date()
    });
    
    io.to(data.roomId).emit('receive-message', message);
  });
  
  socket.on('typing', (data) => {
    socket.to(data.roomId).emit('user-typing', {
      userId: data.userId,
      username: data.username
    });
  });
  
  socket.on('disconnect', () => {
    console.log('Client disconnected:', socket.id);
  });
});

Frontend Implementation

The React frontend uses hooks for state management and Socket.io client for real-time updates:

import { useEffect, useState } from 'react';
import io, { Socket } from 'socket.io-client';

interface Message {
  id: string;
  content: string;
  userId: string;
  username: string;
  timestamp: Date;
}

export const ChatRoom = ({ roomId, userId, username }) => {
  const [socket, setSocket] = useState<Socket | null>(null);
  const [messages, setMessages] = useState<Message[]>([]);
  const [inputValue, setInputValue] = useState('');
  const [typingUsers, setTypingUsers] = useState<string[]>([]);

  useEffect(() => {
    const newSocket = io(process.env.REACT_APP_SERVER_URL);
    setSocket(newSocket);
    
    newSocket.emit('join-room', roomId);
    
    newSocket.on('receive-message', (message: Message) => {
      setMessages(prev => [...prev, message]);
    });
    
    newSocket.on('user-typing', ({ username }) => {
      setTypingUsers(prev => [...prev, username]);
      setTimeout(() => {
        setTypingUsers(prev => prev.filter(u => u !== username));
      }, 3000);
    });
    
    return () => {
      newSocket.disconnect();
    };
  }, [roomId]);

  const sendMessage = () => {
    if (socket && inputValue.trim()) {
      socket.emit('send-message', {
        content: inputValue,
        userId,
        username,
        roomId
      });
      setInputValue('');
    }
  };

  const handleTyping = () => {
    if (socket) {
      socket.emit('typing', { userId, username, roomId });
    }
  };

  return (
    <div className="chat-room">
      <div className="messages">
        {messages.map(msg => (
          <div key={msg.id} className="message">
            <strong>{msg.username}:</strong> {msg.content}
          </div>
        ))}
      </div>
      
      {typingUsers.length > 0 && (
        <div className="typing-indicator">
          {typingUsers.join(', ')} {typingUsers.length === 1 ? 'is' : 'are'} typing...
        </div>
      )}
      
      <input
        value={inputValue}
        onChange={(e) => {
          setInputValue(e.target.value);
          handleTyping();
        }}
        onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
        placeholder="Type a message..."
      />
    </div>
  );
};

Challenges & Solutions

Challenge 1: Message Ordering

Problem: Messages could arrive out of order due to network latency.

Solution: Implemented server-side timestamps and client-side sorting to ensure messages always display in chronological order.

Challenge 2: Connection Stability

Problem: Users would lose connection on network interruptions.

Solution: Added automatic reconnection logic with exponential backoff and message queue to resend failed messages.

Challenge 3: Scalability

Problem: Single server couldn’t handle thousands of concurrent connections.

Solution: Implemented Redis adapter for Socket.io to enable horizontal scaling across multiple server instances.

Lessons Learned

  1. WebSocket lifecycle management is crucial - proper cleanup prevents memory leaks
  2. Optimistic UI updates improve perceived performance
  3. Rate limiting is essential to prevent spam and abuse
  4. Connection state management requires careful handling of edge cases
  5. Testing real-time features requires specialized tools and strategies

Future Enhancements

  • End-to-end encryption for private messages
  • File sharing and image uploads
  • Voice and video calling
  • Message reactions and threading
  • Push notifications for offline users

Try It Out

Check out the live demo or explore the source code on GitHub.

We respect your privacy.

← View All Projects

Related Tutorials

    Ask me anything!