Build a live chat room where messages appear instantly for all connected users. Practice WebSockets, event-driven architecture, and a React frontend.

Requirements

  • Node.js 18+ and npm
  • Express and React basics
  • Understanding of client-server communication

Features

  • Join a chat room with a username
  • Send and receive messages in real time
  • Display online user count
  • Show message timestamps
  • Auto-scroll to the latest message

Step 1: Project Structure

  chat-app/
├── server/
│   ├── package.json
│   └── index.js
└── client/
    ├── package.json
    └── src/
        └── App.jsx
  

Step 2: Socket.io Server

  cd server && npm init -y && npm install express socket.io cors
  

server/index.js

  import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
import cors from 'cors';

const app = express();
app.use(cors());
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: 'http://localhost:5173' } });

const users = new Map();

io.on('connection', socket => {
  console.log(`Connected: ${socket.id}`);
  io.emit('user-count', users.size);

  socket.on('join', username => {
    users.set(socket.id, username);
    io.emit('user-joined', { username, time: new Date().toISOString() });
    io.emit('user-count', users.size);
  });

  socket.on('message', text => {
    const username = users.get(socket.id);
    if (!username || !text.trim()) return;
    io.emit('message', {
      username,
      text: text.trim(),
      time: new Date().toISOString(),
    });
  });

  socket.on('disconnect', () => {
    const username = users.get(socket.id);
    if (username) {
      users.delete(socket.id);
      io.emit('user-left', { username, time: new Date().toISOString() });
      io.emit('user-count', users.size);
    }
  });
});

httpServer.listen(3001, () => console.log('Server on http://localhost:3001'));
  

Add "type": "module" to server/package.json.

Step 3: React Client

  cd client && npm create vite@latest . -- --template react && npm install socket.io-client
  

client/src/App.jsx

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

const socket = io('http://localhost:3001');

export default function App() {
  const [username, setUsername] = useState('');
  const [joined, setJoined] = useState(false);
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [userCount, setUserCount] = useState(0);
  const bottomRef = useRef(null);

  useEffect(() => {
    socket.on('message', msg => setMessages(prev => [...prev, msg]));
    socket.on('user-joined', msg => setMessages(prev => [...prev, { ...msg, system: true }]));
    socket.on('user-left', msg => setMessages(prev => [...prev, { ...msg, system: true }]));
    socket.on('user-count', count => setUserCount(count));
    return () => { socket.off(); };
  }, []);

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  function handleJoin(e) {
    e.preventDefault();
    if (!username.trim()) return;
    socket.emit('join', username.trim());
    setJoined(true);
  }

  function sendMessage(e) {
    e.preventDefault();
    if (!input.trim()) return;
    socket.emit('message', input);
    setInput('');
  }

  if (!joined) {
    return (
      <form onSubmit={handleJoin}>
        <h1>Join Chat</h1>
        <input value={username} onChange={e => setUsername(e.target.value)} placeholder="Your name" required />
        <button type="submit">Enter</button>
      </form>
    );
  }

  return (
    <div className="chat">
      <header><h1>Chat Room</h1><span>{userCount} online</span></header>
      <div className="messages">
        {messages.map((msg, i) => (
          <div key={i} className={msg.system ? 'system' : 'message'}>
            {msg.system ? <em>{msg.username} joined</em> : <><strong>{msg.username}</strong>: {msg.text}</>}
            <small>{new Date(msg.time).toLocaleTimeString()}</small>
          </div>
        ))}
        <div ref={bottomRef} />
      </div>
      <form onSubmit={sendMessage} className="input-bar">
        <input value={input} onChange={e => setInput(e.target.value)} placeholder="Type a message..." />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}
  

Add CSS for .chat, .messages, .input-bar, and .system to style the layout. Run the server (node index.js) and client (npm run dev) in separate terminals, then open two browser tabs to test.

Extension Ideas

  • Multiple rooms — let users create or join named channels
  • Private messages — direct messaging between two users
  • Message history — persist messages in Redis or MongoDB
  • Typing indicators — emit typing events while the user composes
  • File sharing — upload images via a REST endpoint and share URLs
  • Authentication — require login before joining chat rooms