Table of Contents
- Prerequisites
- Project Setup
- Choosing a Real-Time Backend
- Building the React Frontend
- Implementing Real-Time Communication
- Adding Advanced Features
- Testing the Application
- Deployment
- Conclusion
- References
Prerequisites
Before starting, ensure you have the following tools installed:
- Node.js (v14+ recommended) and npm (or yarn)
- Basic knowledge of React (components, hooks like
useStateanduseEffect) - Familiarity with JavaScript (ES6+) and HTML/CSS
- A code editor (e.g., VS Code)
Project Setup
We’ll start by creating a new React project and setting up a basic Node.js server for real-time communication.
Step 1: Create the React Frontend
Open your terminal and run:
npx create-react-app react-realtime-chat
cd react-realtime-chat
Step 2: Install Dependencies
We’ll need socket.io-client (to connect to the Socket.io server) and react-router-dom (for navigation between login and chat screens). Install them with:
npm install socket.io-client react-router-dom
Step 3: Set Up the Node.js Backend
Create a separate folder for the server (or use a subfolder in the project):
mkdir server
cd server
npm init -y
npm install express socket.io cors
Choosing a Real-Time Backend
For real-time communication, we’ll use Socket.io, a library that enables low-latency, bidirectional communication between clients and servers. It works by establishing a WebSocket connection (with fallbacks like HTTP long-polling) and allows the server to broadcast messages to all connected clients instantly.
Alternatives like Firebase Realtime Database or Pusher exist, but Socket.io is lightweight, self-hosted, and ideal for learning the fundamentals of real-time communication.
Building the React Frontend
Setting Up the Project Structure
Organize your React project into folders for components, pages, and utilities:
src/
├── components/ # Reusable UI components (Message, MessageList, etc.)
├── pages/ # Full pages (Login, ChatRoom)
├── utils/ # Helper functions (e.g., format timestamps)
├── App.js # Main app with routing
└── index.js # Entry point
Creating Core Components
1. Login Page (pages/Login.js)
Users will enter a username before joining the chat. Create src/pages/Login.js:
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const Login = () => {
const [username, setUsername] = useState('');
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
if (username.trim()) {
navigate('/chat', { state: { username } }); // Pass username to chat room
}
};
return (
<div className="login-container">
<h1>Join Chat Room</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<button type="submit">Join Chat</button>
</form>
</div>
);
};
export default Login;
2. Chat Room Page (pages/ChatRoom.js)
This page will display messages, a message input, and user info. Create src/pages/ChatRoom.js:
import { useState, useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import io from 'socket.io-client';
import MessageList from '../components/MessageList';
import MessageInput from '../components/MessageInput';
const socket = io('http://localhost:3001'); // Connect to Socket.io server
const ChatRoom = () => {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const location = useLocation();
const { username } = location.state; // Get username from login
const messagesEndRef = useRef(null); // For auto-scrolling to new messages
// Load previous messages (optional: fetch from a database)
useEffect(() => {
// Listen for incoming messages from the server
socket.on('chat message', (message) => {
setMessages((prev) => [...prev, message]);
});
return () => socket.disconnect(); // Cleanup on unmount
}, []);
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = (e) => {
e.preventDefault();
if (input.trim()) {
const message = {
username,
text: input,
timestamp: new Date().toISOString(),
};
socket.emit('chat message', message); // Send message to server
setInput(''); // Clear input
}
};
return (
<div className="chat-container">
<h1>Chat Room</h1>
<p>Welcome, {username}!</p>
<MessageList messages={messages} currentUser={username} />
<div ref={messagesEndRef} /> {/* Scroll target */}
<MessageInput input={input} setInput={setInput} onSubmit={sendMessage} />
</div>
);
};
export default ChatRoom;
3. MessageList Component (components/MessageList.js)
Displays a list of messages. Create src/components/MessageList.js:
import Message from './Message';
const MessageList = ({ messages, currentUser }) => {
return (
<div className="message-list">
{messages.map((msg, index) => (
<Message key={index} message={msg} isCurrentUser={msg.username === currentUser} />
))}
</div>
);
};
export default MessageList;
4. Message Component (components/Message.js)
Renders a single message with the username, text, and timestamp. Create src/components/Message.js:
import { formatTimestamp } from '../utils/format';
const Message = ({ message, isCurrentUser }) => {
return (
<div className={`message ${isCurrentUser ? 'current-user' : ''}`}>
<div className="message-header">
<span className="username">{message.username}</span>
<span className="timestamp">{formatTimestamp(message.timestamp)}</span>
</div>
<div className="message-text">{message.text}</div>
</div>
);
};
export default Message;
5. MessageInput Component (components/MessageInput.js)
Handles message input and submission. Create src/components/MessageInput.js:
const MessageInput = ({ input, setInput, onSubmit }) => {
return (
<form onSubmit={onSubmit} className="message-input">
<input
type="text"
placeholder="Type a message..."
value={input}
onChange={(e) => setInput(e.target.value)}
required
/>
<button type="submit">Send</button>
</form>
);
};
export default MessageInput;
6. Format Timestamp Utility (utils/format.js)
Create src/utils/format.js to format timestamps:
export const formatTimestamp = (isoString) => {
const date = new Date(isoString);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
Setting Up Routing in App.js
Update src/App.js to use react-router-dom for navigation between login and chat:
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Login from './pages/Login';
import ChatRoom from './pages/ChatRoom';
import './App.css';
function App() {
return (
<Router>
<div className="App">
<Routes>
<Route path="/" element={<Login />} />
<Route path="/chat" element={<ChatRoom />} />
</Routes>
</div>
</Router>
);
}
export default App;
Implementing Real-Time Communication
Setting Up the Socket.io Server
In the server folder, create index.js (the Socket.io server):
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
const app = express();
app.use(cors());
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: "http://localhost:3000", // Allow React frontend
methods: ["GET", "POST"]
}
});
// Track connected users (optional)
const connectedUsers = new Set();
io.on('connection', (socket) => {
console.log('A user connected:', socket.id);
// Listen for "chat message" events from clients
socket.on('chat message', (message) => {
io.emit('chat message', message); // Broadcast to all clients
});
// Handle disconnection
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
const PORT = 3001;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Connecting React to the Socket.io Server
In ChatRoom.js, we already imported socket.io-client and connected to the server with:
import io from 'socket.io-client';
const socket = io('http://localhost:3001');
The server listens for chat message events and broadcasts them to all clients. The React app emits chat message events when the user sends a message and listens for incoming messages to update the messages state.
Adding Advanced Features
User Authentication (Username Input)
We already implemented a basic login flow with react-router-dom. For production, you might add password validation or integrate OAuth (e.g., Firebase Auth, Auth0), but a simple username input works for this tutorial.
Message Timestamps and User Avatars
Timestamps
We already use formatTimestamp to display message times. Enhance it to show dates for older messages:
// utils/format.js
export const formatTimestamp = (isoString) => {
const date = new Date(isoString);
const now = new Date();
const isToday = date.toDateString() === now.toDateString();
if (isToday) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
};
Avatars
Add simple avatars using the username’s initials. Update Message.js:
<div className="message-avatar">{message.username.charAt(0).toUpperCase()}</div>
Typing Indicators
Add “User is typing…” functionality by emitting “typing” events when the user types:
Update ChatRoom.js:
const [typingUser, setTypingUser] = useState(null);
// Emit "typing" event when input changes
useEffect(() => {
if (input.trim()) {
socket.emit('typing', username);
} else {
socket.emit('stop typing');
}
// Listen for "typing" events from server
socket.on('typing', (user) => {
setTypingUser(user);
// Clear typing indicator after 3 seconds
const timeout = setTimeout(() => setTypingUser(null), 3000);
return () => clearTimeout(timeout);
});
socket.on('stop typing', () => setTypingUser(null));
}, [input, username, socket]);
Update the Server (server/index.js)
Add handlers for “typing” and “stop typing” events:
socket.on('typing', (username) => {
socket.broadcast.emit('typing', username); // Broadcast to others
});
socket.on('stop typing', () => {
socket.broadcast.emit('stop typing');
});
Display Typing Indicator in ChatRoom.js:
{typingUser && <div className="typing-indicator">{typingUser} is typing...</div>}
Testing the Application
-
Start the Socket.io server:
cd server node index.js -
Start the React app in a new terminal:
cd react-realtime-chat npm start -
Open
http://localhost:3000in multiple browsers/tabs. Enter a username, send messages, and verify they appear in real-time across tabs. Test typing indicators by typing in one tab and watching another.
Deployment
Deploy the Socket.io Server
Host the Node.js server on platforms like Heroku, Render, or Railway:
- For Heroku:
- Create a
Procfilein the server folder:web: node index.js - Push to Heroku:
git push heroku main
- Create a
Deploy the React Frontend
Host the React app on Vercel or Netlify:
- Push your React project to GitHub.
- Connect to Vercel/Netlify and deploy from the repo.
Update the Socket.io connection URL in ChatRoom.js to use your deployed server (e.g., https://your-server.herokuapp.com).
Conclusion
You’ve built a real-time chat app with React and Socket.io! Key features include real-time messaging, typing indicators, and user authentication. To enhance it further, consider adding:
- File attachments (images, documents)
- Group chat rooms
- Read receipts
- Message editing/deletion