Real-Time Chat Application
Learning Objectives
- By the end of this project, you will be able to:
- - Understand WebSockets and real-time communication
- - Use Socket.io for bidirectional communication
- - Manage user connections and disconnections
- - Handle real-time messaging
- - Build interactive chat interfaces
- - Manage chat rooms and channels
- - Handle user presence
Project 4.1: Real-Time Chat Application
Project Overview
Build a real-time chat application using WebSockets and Socket.io. This project will help you practice real-time communication, WebSocket connections, user management, and building interactive applications.
Learning Objectives
By the end of this project, you will be able to:
- Understand WebSockets and real-time communication
- Use Socket.io for bidirectional communication
- Manage user connections and disconnections
- Handle real-time messaging
- Build interactive chat interfaces
- Manage chat rooms and channels
- Handle user presence
Project Requirements
Core Features
- Real-Time Messaging: Send and receive messages instantly
- User Management: Join/leave, user list
- Chat Rooms: Multiple chat rooms
- Typing Indicators: Show when users are typing
- User Presence: Show online/offline status
- Message History: Store and display message history
- Notifications: Notify users of new messages
- Responsive Design: Works on all devices
Technical Requirements
- Node.js backend with Socket.io
- React frontend with Socket.io client
- Real-time WebSocket connections
- User session management
- Message storage (optional database)
- Clean UI/UX
Project Setup
Backend Setup
# Create backend directory
mkdir chat-backend
cd chat-backend
# Initialize npm
npm init -y
# Install dependencies
npm install express socket.io cors dotenv
npm install --save-dev nodemon
# Create project structure
mkdir src
Frontend Setup
# Create React app
npx create-react-app chat-frontend
cd chat-frontend
# Install Socket.io client
npm install socket.io-client
# Start development server
npm start
Project Structure
chat-application/
├── backend/
│ ├── src/
│ │ ├── server.js
│ │ └── socketHandlers.js
│ ├── .env
│ └── package.json
└── frontend/
├── src/
│ ├── components/
│ │ ├── ChatRoom.jsx
│ │ ├── MessageList.jsx
│ │ ├── MessageInput.jsx
│ │ ├── UserList.jsx
│ │ └── Login.jsx
│ ├── hooks/
│ │ └── useSocket.js
│ ├── App.jsx
│ └── index.js
└── package.json
Step-by-Step Implementation
Backend: Server Setup
// backend/src/server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
require('dotenv').config();
const app = express();
const server = http.createServer(app);
// Configure CORS for Socket.io
const io = new Server(server, {
cors: {
origin: process.env.CLIENT_URL || 'http://localhost:3000',
methods: ['GET', 'POST']
}
});
app.use(cors());
app.use(express.json());
// Store connected users
const users = new Map();
const messages = [];
// Socket.io connection handling
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
// User joins
socket.on('user:join', (userData) => {
users.set(socket.id, {
id: socket.id,
username: userData.username,
room: userData.room || 'general'
});
// Join room
socket.join(userData.room || 'general');
// Notify others
socket.to(userData.room || 'general').emit('user:joined', {
username: userData.username,
message: `${userData.username} joined the chat`
});
// Send current users
const roomUsers = Array.from(users.values())
.filter(user => user.room === (userData.room || 'general'));
io.to(userData.room || 'general').emit('users:list', roomUsers);
// Send message history
const roomMessages = messages.filter(
msg => msg.room === (userData.room || 'general')
);
socket.emit('messages:history', roomMessages);
});
// Send message
socket.on('message:send', (messageData) => {
const user = users.get(socket.id);
if (user) {
const message = {
id: Date.now(),
username: user.username,
text: messageData.text,
room: user.room,
timestamp: new Date().toISOString()
};
messages.push(message);
// Send to room
io.to(user.room).emit('message:receive', message);
}
});
// Typing indicator
socket.on('typing:start', () => {
const user = users.get(socket.id);
if (user) {
socket.to(user.room).emit('typing:start', {
username: user.username
});
}
});
socket.on('typing:stop', () => {
const user = users.get(socket.id);
if (user) {
socket.to(user.room).emit('typing:stop', {
username: user.username
});
}
});
// User disconnects
socket.on('disconnect', () => {
const user = users.get(socket.id);
if (user) {
users.delete(socket.id);
// Notify others
socket.to(user.room).emit('user:left', {
username: user.username,
message: `${user.username} left the chat`
});
// Update user list
const roomUsers = Array.from(users.values())
.filter(u => u.room === user.room);
io.to(user.room).emit('users:list', roomUsers);
}
console.log('User disconnected:', socket.id);
});
});
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Frontend: Socket Hook
// frontend/src/hooks/useSocket.js
import { useEffect, useState } from 'react';
import io from 'socket.io-client';
const SOCKET_URL = process.env.REACT_APP_SOCKET_URL || 'http://localhost:5000';
export function useSocket() {
const [socket, setSocket] = useState(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
const newSocket = io(SOCKET_URL);
newSocket.on('connect', () => {
setConnected(true);
});
newSocket.on('disconnect', () => {
setConnected(false);
});
setSocket(newSocket);
return () => {
newSocket.close();
};
}, []);
return { socket, connected };
}
Frontend: Login Component
// frontend/src/components/Login.jsx
import { useState } from 'react';
import './Login.css';
function Login({ onLogin }) {
const [username, setUsername] = useState('');
const [room, setRoom] = useState('general');
const handleSubmit = (e) => {
e.preventDefault();
if (username.trim()) {
onLogin(username.trim(), room);
}
};
return (
<div className="login-container">
<div className="login-box">
<h1>Join Chat</h1>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Username</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
required
/>
</div>
<div className="form-group">
<label>Room</label>
<select
value={room}
onChange={(e) => setRoom(e.target.value)}
>
<option value="general">General</option>
<option value="random">Random</option>
<option value="tech">Tech</option>
<option value="gaming">Gaming</option>
</select>
</div>
<button type="submit" className="btn btn-primary">
Join Chat
</button>
</form>
</div>
</div>
);
}
export default Login;
Frontend: Message List Component
// frontend/src/components/MessageList.jsx
import { useEffect, useRef } from 'react';
import './MessageList.css';
function MessageList({ messages, currentUser }) {
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
return (
<div className="message-list">
{messages.map(message => (
<div
key={message.id}
className={`message ${message.username === currentUser ? 'own' : ''}`}
>
<div className="message-header">
<span className="message-username">{message.username}</span>
<span className="message-time">
{new Date(message.timestamp).toLocaleTimeString()}
</span>
</div>
<div className="message-text">{message.text}</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
);
}
export default MessageList;
Frontend: Message Input Component
// frontend/src/components/MessageInput.jsx
import { useState, useEffect, useRef } from 'react';
import './MessageInput.css';
function MessageInput({ socket, currentUser, room }) {
const [message, setMessage] = useState('');
const [isTyping, setIsTyping] = useState(false);
const typingTimeoutRef = useRef(null);
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
};
}, []);
const handleChange = (e) => {
setMessage(e.target.value);
if (!isTyping) {
setIsTyping(true);
socket.emit('typing:start');
}
clearTimeout(typingTimeoutRef.current);
typingTimeoutRef.current = setTimeout(() => {
setIsTyping(false);
socket.emit('typing:stop');
}, 1000);
};
const handleSubmit = (e) => {
e.preventDefault();
if (message.trim()) {
socket.emit('message:send', { text: message.trim() });
setMessage('');
setIsTyping(false);
socket.emit('typing:stop');
}
};
return (
<form className="message-input" onSubmit={handleSubmit}>
<input
type="text"
value={message}
onChange={handleChange}
placeholder="Type a message..."
className="message-input-field"
/>
<button type="submit" className="btn btn-send">
Send
</button>
</form>
);
}
export default MessageInput;
Frontend: User List Component
// frontend/src/components/UserList.jsx
import './UserList.css';
function UserList({ users, currentUser }) {
return (
<div className="user-list">
<h3>Online Users ({users.length})</h3>
<ul>
{users.map(user => (
<li key={user.id} className={user.username === currentUser ? 'current' : ''}>
<span className="user-indicator"></span>
{user.username}
{user.username === currentUser && ' (You)'}
</li>
))}
</ul>
</div>
);
}
export default UserList;
Frontend: Chat Room Component
// frontend/src/components/ChatRoom.jsx
import { useState, useEffect } from 'react';
import { useSocket } from '../hooks/useSocket';
import MessageList from './MessageList';
import MessageInput from './MessageInput';
import UserList from './UserList';
import './ChatRoom.css';
function ChatRoom({ username, room, onLeave }) {
const { socket, connected } = useSocket();
const [messages, setMessages] = useState([]);
const [users, setUsers] = useState([]);
const [typingUsers, setTypingUsers] = useState([]);
const [systemMessage, setSystemMessage] = useState('');
useEffect(() => {
if (socket && username) {
// Join room
socket.emit('user:join', { username, room });
// Listen for messages
socket.on('message:receive', (message) => {
setMessages(prev => [...prev, message]);
});
// Listen for message history
socket.on('messages:history', (history) => {
setMessages(history);
});
// Listen for user list
socket.on('users:list', (userList) => {
setUsers(userList);
});
// Listen for user joined
socket.on('user:joined', (data) => {
setSystemMessage(`${data.username} joined the chat`);
setTimeout(() => setSystemMessage(''), 3000);
});
// Listen for user left
socket.on('user:left', (data) => {
setSystemMessage(`${data.username} left the chat`);
setTimeout(() => setSystemMessage(''), 3000);
});
// Listen for typing
socket.on('typing:start', (data) => {
setTypingUsers(prev => {
if (!prev.includes(data.username)) {
return [...prev, data.username];
}
return prev;
});
});
socket.on('typing:stop', (data) => {
setTypingUsers(prev => prev.filter(u => u !== data.username));
});
return () => {
socket.off('message:receive');
socket.off('messages:history');
socket.off('users:list');
socket.off('user:joined');
socket.off('user:left');
socket.off('typing:start');
socket.off('typing:stop');
};
}
}, [socket, username, room]);
return (
<div className="chat-room">
<div className="chat-header">
<div>
<h2>Room: {room}</h2>
<span className={`connection-status ${connected ? 'connected' : 'disconnected'}`}>
{connected ? '● Connected' : '○ Disconnected'}
</span>
</div>
<button onClick={onLeave} className="btn btn-leave">
Leave Room
</button>
</div>
<div className="chat-content">
<div className="chat-messages">
<MessageList messages={messages} currentUser={username} />
{systemMessage && (
<div className="system-message">{systemMessage}</div>
)}
{typingUsers.length > 0 && (
<div className="typing-indicator">
{typingUsers.join(', ')} {typingUsers.length === 1 ? 'is' : 'are'} typing...
</div>
)}
<MessageInput socket={socket} currentUser={username} room={room} />
</div>
<UserList users={users} currentUser={username} />
</div>
</div>
);
}
export default ChatRoom;
Frontend: App Component
// frontend/src/App.jsx
import { useState } from 'react';
import Login from './components/Login';
import ChatRoom from './components/ChatRoom';
import './App.css';
function App() {
const [username, setUsername] = useState(null);
const [room, setRoom] = useState(null);
const handleLogin = (user, selectedRoom) => {
setUsername(user);
setRoom(selectedRoom);
};
const handleLeave = () => {
setUsername(null);
setRoom(null);
};
return (
<div className="app">
{!username ? (
<Login onLogin={handleLogin} />
) : (
<ChatRoom username={username} room={room} onLeave={handleLeave} />
)}
</div>
);
}
export default App;
Features Implementation
WebSockets
- Real-Time Connection: Bidirectional communication
- Event-Based: Emit and listen to events
- Room Management: Join/leave rooms
- Connection Status: Track connection state
Real-Time Communication
- Instant Messaging: Messages sent/received instantly
- Typing Indicators: Show when users are typing
- User Presence: Show online users
- System Messages: Notify of user joins/leaves
User Management
- User List: Display connected users
- User Identification: Track users by socket ID
- Room Assignment: Users belong to rooms
- Disconnect Handling: Clean up on disconnect
Testing Your Application
Manual Testing Checklist
- [ ] Connect to chat
- [ ] Send messages
- [ ] Receive messages
- [ ] See user list
- [ ] See typing indicators
- [ ] Join different rooms
- [ ] See system messages
- [ ] Handle disconnection
- [ ] Multiple users
- [ ] Message history
Exercise: Chat App
Instructions:
- Set up backend and frontend
- Implement all features
- Test with multiple users
- Add enhancements
- Deploy application
Enhancement Ideas:
- Add private messaging
- Add file sharing
- Add emoji support
- Add message reactions
- Add user avatars
- Add message search
- Add message editing/deleting
- Add voice/video chat
Common Issues and Solutions
Issue: Connection fails
Solution: Check CORS configuration and Socket.io URL.
Issue: Messages not appearing
Solution: Verify event names match on emit and on.
Issue: Users not updating
Solution: Ensure user list is updated on join/leave events.
Quiz: WebSockets
-
WebSockets:
- A) Real-time communication
- B) Request-response only
- C) Both
- D) Neither
-
Socket.io:
- A) WebSocket library
- B) HTTP library
- C) Both
- D) Neither
-
Real-time:
- A) Instant updates
- B) Delayed updates
- C) Both
- D) Neither
-
Rooms:
- A) Group users
- B) Don't group users
- C) Both
- D) Neither
-
Typing indicators:
- A) Show user activity
- B) Don't show activity
- C) Both
- D) Neither
Answers:
- A) Real-time communication
- A) WebSocket library
- A) Instant updates
- A) Group users
- A) Show user activity
Key Takeaways
- WebSockets: Real-time bidirectional communication
- Socket.io: Easy WebSocket implementation
- Real-Time: Instant updates without polling
- User Management: Track connected users
- Best Practice: Handle connections and disconnections
Next Steps
Congratulations! You've built a Real-Time Chat Application. You now know:
- How to use WebSockets
- How to implement Socket.io
- How to handle real-time communication
- How to manage users
What's Next?
- Project 4.2: Social Media Clone
- Learn complex state management
- Build social features
- Handle image uploads
Project completed! You're ready to move on to the next project.
Course Navigation
Interactive Web Pages
Frontend Applications
Full-Stack Applications
Advanced Projects
- Real-Time Chat Application
- Social Media Clone
Advanced Projects
- Real-Time Chat Application
- Social Media Clone