Building a Real-Time Chat Application with React,
WebSockets, and MongoDB
Chat applications are ubiquitous in today's digital world, enabling instant communication between individuals
and groups. The magic behind their real-time nature often lies in WebSockets, a communication protocol that
provides a full-duplex communication channel over a single TCP connection. Unlike traditional HTTP requests
(which are stateless and often short-lived), WebSockets allow the server to push data to the client without the
client explicitly requesting it, making them ideal for live updates like chat messages.
In this blog post, we'll walk through building a simple real-time chat application using:
- React.js: For building a dynamic and interactive user interface on the frontend.
- Node.js & Express: For creating a robust and scalable backend server.
- WebSockets (using the ws library): For establishing real-time communication
between the client and server.
- MongoDB: For storing chat messages persistently in a NoSQL database.
1. Understanding the Core Concepts
Before we dive into coding, let's briefly understand the key components:
- WebSockets: A protocol providing full-duplex communication channels over a single
TCP connection. This means both client and server can send and receive data simultaneously.
- Node.js: A JavaScript runtime built on Chrome's V8 JavaScript engine, perfect for
building fast, scalable network applications.
- Express.js: A minimal and flexible Node.js web application framework that
provides a robust set of features for web and mobile applications.
- MongoDB: A popular NoSQL document database that stores data in flexible, JSON-like
documents, making it easy to integrate with JavaScript applications.
- React.js: A JavaScript library for building user interfaces, known for its
component-based architecture and declarative approach.
2. Prerequisites
Before you begin, ensure you have the following installed on your system:
- Node.js and npm: Download and install from
nodejs.org.
- MongoDB: Download and install from
mongodb.com.
You'll also need to have a MongoDB instance running (either locally or a cloud-hosted one like MongoDB Atlas).
- Text Editor/IDE: Visual Studio Code, Sublime Text, or any editor of your choice.
3. Backend Setup (Node.js, Express & WebSockets)
We'll start by building the backend server that will handle WebSocket connections, manage messages, and interact with the database.
Step 3.1: Initialize the Node.js Project
Create a new folder for your project and initialize a Node.js project:
mkdir chat-app
cd chat-app
mkdir backend
cd backend
npm init -y
Step 3.2: Install Dependencies
Install Express, ws (for WebSockets), and mongoose (for MongoDB interaction):
npm install express ws mongoose dotenv
dotenv
will be used to manage environment variables (like MongoDB connection string).
Step 3.3: Create the Server File (server.js)
In the backend folder, create a file named server.js:
// backend/server.js
require('dotenv').config();
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const mongoose = require('mongoose');
const app = express();
const port = process.env.PORT || 8080;
// --- MongoDB Connection ---
const mongoURI = process.env.MONGO_URI;
mongoose.connect(mongoURI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('MongoDB connected successfully!'))
.catch(err => console.error('MongoDB connection error:', err));
// Define Message Schema
const MessageSchema = new mongoose.Schema({
sender: { type: String, required: true },
content: { type: String, required: true },
timestamp: { type: Date, default: Date.now },
});
const Message = mongoose.model('Message', MessageSchema);
// --- HTTP Server Setup ---
const server = http.createServer(app);
// Basic Express route (optional)
app.get('/', (req, res) => {
res.send('Chat App Backend is Running!');
});
// --- WebSocket Server Setup ---
const wss = new WebSocket.Server({ server });
const clients = new Set();
wss.on('connection', ws => {
console.log('Client connected');
clients.add(ws);
Message.find().sort({ timestamp: 1 }).limit(100)
.then(messages => {
messages.forEach(msg => {
ws.send(JSON.stringify(msg));
});
})
.catch(err => console.error('Error fetching past messages:', err));
ws.on('message', async message => {
console.log(`Received message: ${message}`);
try {
const parsedMessage = JSON.parse(message);
const newMessage = new Message({
sender: parsedMessage.sender || 'Anonymous',
content: parsedMessage.content,
timestamp: new Date()
});
await newMessage.save();
console.log('Message saved to DB:', newMessage);
const messageToBroadcast = JSON.stringify(newMessage);
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(messageToBroadcast);
}
});
} catch (error) {
console.error('Error processing message:', error);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' }));
}
}
});
ws.on('close', () => {
console.log('Client disconnected');
clients.delete(ws);
});
ws.on('error', error => {
console.error('WebSocket error:', error);
});
});
server.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
console.log(`WebSocket server is running on ws://localhost:${port}`);
});
Step 3.4: Create .env File
In the backend folder, create a .env
file and add your MongoDB connection string:
# backend/.env
MONGO_URI=your_mongodb_connection_string
PORT=8080
Step 3.5: Run the Backend Server
cd backend
node server.js
You should see "MongoDB connected successfully!" and "Server is running on http://localhost:8080" in your console.
4. Frontend Setup (React.js)
Now, let's build the React application that will serve as our chat client.
Step 4.1: Create a React App
Open a new terminal, navigate back to your chat-app
root directory, and create a new React app:
cd .. # Go back to chat-app root
npx create-react-app frontend
cd frontend
Step 4.2: Clean Up and Structure
Remove unnecessary files from src
(like App.test.js, logo.svg, reportWebVitals.js, setupTests.js).
Your src folder should primarily contain:
index.js
App.js
index.css
(or App.css
)
Step 4.3: Update src/index.css
Add some basic global styles for better aesthetics:
/* src/index.css */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
body {
margin: 0;
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
box-sizing: border-box;
}
#root {
width: 100%;
max-width: 800px;
}
Step 4.4: Create the ChatApp Component (src/App.js)
This will be the main React component managing WebSocket connections, user input, and rendering messages:
// frontend/src/App.js
import React, { useState, useEffect, useRef } from 'react';
import './App.css'; // We'll create this CSS file next
function App() {
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState('');
const [username, setUsername] = useState('');
const [isUsernameSet, setIsUsernameSet] = useState(false);
const ws = useRef(null);
const messagesEndRef = useRef(null);
const WEBSOCKET_URL = 'ws://localhost:8080';
useEffect(() => {
if (!isUsernameSet) return;
ws.current = new WebSocket(WEBSOCKET_URL);
ws.current.onopen = () => console.log('WebSocket connection opened');
ws.current.onmessage = event => {
try {
const receivedMessage = JSON.parse(event.data);
setMessages(prev => [...prev, receivedMessage]);
} catch (error) {
console.error('Failed to parse message:', error);
}
};
ws.current.onclose = () => console.log('WebSocket closed');
ws.current.onerror = error => console.error('WebSocket error:', error);
return () => {
if (ws.current?.readyState === WebSocket.OPEN) ws.current.close();
};
}, [isUsernameSet]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = (e) => {
e.preventDefault();
if (inputMessage.trim() && ws.current?.readyState === WebSocket.OPEN) {
const messagePayload = {
sender: username,
content: inputMessage.trim(),
};
ws.current.send(JSON.stringify(messagePayload));
setInputMessage('');
}
};
const handleUsernameSubmit = (e) => {
e.preventDefault();
if (username.trim()) setIsUsernameSet(true);
};
if (!isUsernameSet) {
return (
<div className="chat-container username-screen">
<h1 className="chat-title">Welcome to Chat!</h1>
<form onSubmit={handleUsernameSubmit} className="username-form">
<input
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
className="username-input"
/>
<button type="submit" className="set-username-button">
Start Chatting
</button>
</form>
</div>
);
}
return (
<div className="chat-container">
<h1 className="chat-title">Real-Time Chat</h1>
<div className="messages-window">
{messages.map((msg, index) => (
<div key={index} className={`message ${msg.sender === username ? 'sent' : 'received'}`}>
<div className="message-header">
<span className="message-sender">{msg.sender}</span>
<span className="message-time">{new Date(msg.timestamp).toLocaleTimeString()}</span>
</div>
<div className="message-content">{msg.content}</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<form onSubmit={sendMessage} className="message-input-form">
<input
type="text"
placeholder="Type your message..."
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
className="message-input"
/>
<button type="submit" className="send-button">
Send
</button>
</form>
<div className="current-user">Logged in as: <strong>{username}</strong></div>
</div>
);
}
export default App;
Step 4.5: Update src/App.css
Add the following styles to design your chat interface:
/* src/App.css */
.chat-container {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
padding: 30px;
display: flex;
flex-direction: column;
max-height: 90vh; /* Limit height to prevent excessive scrolling */
overflow: hidden; /* Hide scrollbar for the container itself */
width: 100%;
}
.chat-title {
text-align: center;
color: #333;
margin-bottom: 25px;
font-size: 2.2em;
font-weight: 700;
}
/* Username screen styling */
.username-screen {
align-items: center;
justify-content: center;
height: 50vh; /* Center content vertically in this screen */
}
.username-form {
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
max-width: 300px;
}
.username-input {
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 1em;
outline: none;
transition: border-color 0.3s ease;
}
.username-input:focus {
border-color: #007bff;
}
.set-username-button {
background-color: #007bff;
color: white;
border: none;
padding: 12px 20px;
border-radius: 8px;
font-size: 1.1em;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.set-username-button:hover {
background-color: #0056b3;
transform: translateY(-2px);
}
/* Messages Window */
.messages-window {
flex-grow: 1; /* Allows it to take available space */
overflow-y: auto; /* Enable scrolling for messages */
padding-right: 10px; /* Space for scrollbar */
margin-bottom: 20px;
border: 1px solid #eee;
border-radius: 8px;
padding: 15px;
background-color: #f9fafa;
min-height: 300px; /* Minimum height for the message window */
}
.message {
margin-bottom: 15px;
padding: 10px 15px;
border-radius: 10px;
max-width: 80%; /* Limit message bubble width */
word-wrap: break-word; /* Break long words */
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.message.sent {
background-color: #dcf8c6; /* Light green for sent messages */
align-self: flex-end; /* Align to the right */
margin-left: auto; /* Push to the right */
border-bottom-right-radius: 2px; /* Small corner for sent */
}
.message.received {
background-color: #e0e0e0; /* Light gray for received messages */
align-self: flex-start; /* Align to the left */
margin-right: auto; /* Push to the left */
border-bottom-left-radius: 2px; /* Small corner for received */
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 0.85em;
color: #666;
}
.message-sender {
font-weight: 500;
color: #007bff; /* Highlight sender */
}
.message-content {
font-size: 1em;
line-height: 1.4;
color: #333;
}
/* Message Input Form */
.message-input-form {
display: flex;
gap: 10px;
}
.message-input {
flex-grow: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 1em;
outline: none;
transition: border-color 0.3s ease;
}
.message-input:focus {
border-color: #007bff;
}
.send-button {
background-color: #28a745; /* Green send button */
color: white;
border: none;
padding: 12px 20px;
border-radius: 8px;
font-size: 1em;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.send-button:hover {
background-color: #218838;
transform: translateY(-2px);
}
.current-user {
text-align: right;
margin-top: 15px;
font-size: 0.9em;
color: #555;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.chat-container {
padding: 20px;
max-width: 95%;
}
.chat-title {
font-size: 1.8em;
}
.username-form {
max-width: 90%;
}
.message-input-form {
flex-direction: column;
gap: 15px;
}
.send-button {
width: 100%;
padding: 15px;
}
}
Step 4.6: Run the Frontend App
Make sure your backend server is still running. Open a new terminal, navigate to the frontend directory and start the app:
cd frontend
npm start
This will open your chat application in the browser, typically at
http://localhost:3000.
5. How It All Connects
-
User Enters Username: The React app first prompts the user for a username. Once set, it attempts to establish a WebSocket connection.
-
WebSocket Connection: The React app's
useEffect
hook connects to
ws://localhost:8080
.
-
Backend Handles Connection:
- The Node.js
wss.on('connection')
event fires.
- The server adds the new client’s WebSocket instance to its
clients
set.
- It fetches the last 100 messages from MongoDB and sends them individually to the newly connected client, ensuring new users see chat history.
-
Sending Messages:
- When a user types a message in React and clicks "Send", the
sendMessage
function sends a JSON string
{"{"}"sender": "username", "content": "message"{"}"}
through the WebSocket using
ws.current.send()
.
-
Backend Receives and Broadcasts:
- The Node.js
ws.on('message')
event receives the message from the client.
- It parses the message and saves it to the
messages
collection in MongoDB.
- After saving, the server iterates through all connected clients and broadcasts the saved message (now including
_id
and timestamp
) to everyone.
-
Frontend Receives Messages:
- Each connected React client’s
ws.current.onmessage
event listener receives the broadcasted message.
- It parses the JSON message and updates the
messages
state in React.
- The React UI re-renders, displaying the new message.
- The
useEffect
hook for scrolling ensures the chat window always shows the latest message.
-
Disconnection: When a client closes the browser or tab, the
ws.on('close')
event fires on the server, and the client's WebSocket instance is removed from the
clients
set.
6. Conclusion and Next Steps
You've successfully built a basic real-time chat application using React, Node.js,
WebSockets, and MongoDB! This project demonstrates fundamental concepts of full-stack development,
real-time communication, and data persistence.
Here are some ideas for improving and expanding your chat app:
- User Authentication: Implement user registration and login to manage distinct user identities. You could use JWT (JSON Web Tokens) for this.
- Chat Rooms/Channels: Allow users to create and join different chat rooms, managing separate message streams and broadcasting per room.
- Typing Indicators: Show when other users are typing.
- Emojis/Rich Text: Add support for emojis and basic text formatting.
- File Sharing: Allow users to share images or other files.
- Read Receipts: Indicate when messages have been read by recipients.
- Error Handling and UI Feedback: Provide visual feedback for network errors or message delivery failures.
- Deployment: Deploy your backend to a cloud platform (e.g., Heroku, AWS, DigitalOcean) and your frontend (e.g., Netlify, Vercel).
- Scalability: For larger-scale apps, use a more robust WebSocket library like Socket.IO which offers built-in support for rooms, reconnection, and more.
This project is a solid foundation for building more complex real-time applications.
Happy coding!