Real-time features like chat and notifications are becoming essential in modern web apps. Socket.IO makes it easy to add these with live, two-way communication. In this guide, you’ll learn how to build a real-time chat app using React and Socket.IO, and integrate it into Liferay as a Client Extension. Whether you’re new to real-time or Liferay CE, this step-by-step tutorial will help you get started quickly and efficiently. Let’s build something live! ⚡
Why Socket IO?
Socket.IO allows real-time, bi-directional communication between the browser and server. It’s reliable, fast, and supports features like auto-reconnect and broadcasting-perfect for chat apps.
Think of it this way
- Socket.IO is like a super-smart walkie-talkie for websites.
- It helps two people (or more) talk to each other instantly on the internet, like sending messages, emojis, or game moves, right away without waiting or refreshing.
Prerequisites
You should be familiar with:
- React 18 +
- Liferay 7.4+
- Node.js and NPM (Basic usage, installing packages)
Project Setup
Needed Dependencies:
- Node version: 22.13.1
- NPM version: 10.9.2
Install both using
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
nvm install 22.13.1
Set it as the default using (if having multiple versions installed):
nvm alias default 22.13.1
node -v # should show v22.13.1
npm -v # 10.9.2
Install Yarn:
sudo npm install -g yarn
yarn --version
Setting Up the Socket.IO Server
To enable real-time communication between users, we need a backend server that can manage WebSocket connections. We’ll use Node.js, Express, and Socket.IO to build a lightweight and efficient Socket.IO server.
Initialize a Node.js Project
First, create a new folder and initialize a Node.js project:
mkdir socket-server
cd socket-server
Install Required Packages
Install Express and Socket.IO:
yarn add express socket.io cors
Or with npm
npm install express socket.io cors
- express: Minimalist web server framework.
- socket.io: WebSocket wrapper for real-time communication.
- cors: Enables cross-origin communication (important when serving React via Liferay domain).
Create the Socket.IO Server
Create a file named index.js:
Add the following code:
import { Server } from "socket.io";
import cors from "cors";
import { createServer } from "http";
import express from "express";
const app = express();
const server = createServer(app);
app.use(cors());
const io = new Server(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
// Simple map of userId -> socketId
let onlineUsers = {};
io.on("connection", (socket) => {
console.log(`New connection: ${socket.id}`);
// When a user joins
socket.on("join_chat", ({ userId, userName }) => {
if (userId && userName) {
onlineUsers[userId] = { userName, socketId: socket.id };
console.log(`${userName} (${userId}) is online`);
io.emit("update_user_status", onlineUsers);
}
});
// Listen for messages
socket.on("send_message", (data) => {
console.log("Message received:", data);
io.emit("receive_message", data);
});
// When a socket disconnects
socket.on("disconnect", () => {
console.log(`Socket ${socket.id} disconnected`);
// Find the user and remove them
const userId = Object.keys(onlineUsers).find(
(id) => onlineUsers[id].socketId === socket.id
);
if (userId) {
console.log(`Removing user ${userId} from online users`);
delete onlineUsers[userId];
io.emit("update_user_status", onlineUsers);
}
});
});
server.listen(3001, '0.0.0.0', () => {
console.log("Socket.IO Server running on port 3001");
});
Here, I have also added code for showing the online users so you may show the online offline status accordingly, as Socket, when the screen refreshes, disconnects the user and reconnects it.
Run the Server
Use Node to start the server:
node index.js
You should see:
Socket.IO server running on port 3001
Folder Structure Summary:
Create a React Liferay client extension for running the chat application.
Add this to app.jsx in react app
import React, { useEffect, useState } from "react";
import { io } from "socket.io-client";
const socket = io("http://localhost:3001");
export default function ChatApp() {
const [userId, setUserId] = useState("");
const [receiverId, setReceiverId] = useState("");
const [message, setMessage] = useState("");
const [messages, setMessages] = useState([]);
const [joined, setJoined] = useState(false);
useEffect(() => {
socket.on("receive_message", (data) => {
if (data.receiverId === userId || data.userId === userId) {
setMessages((prev) => [...prev, data]);
}
});
return () => {
socket.off("receive_message");
};
}, [userId]);
const joinChat = () => {
if (userId.trim()) {
socket.emit("join_chat", { userId });
setJoined(true);
}
};
const sendMessage = () => {
if (message.trim() && receiverId.trim()) {
const data = {
userId,
receiverId,
message,
timestamp: new Date().toISOString(),
};
socket.emit("send_message", data);
setMessage("");
}
};
if (!joined) {
return (
<div className="container py-5">
<h2 className="mb-4">Join Chat</h2>
<input
type="text"
className="form-control mb-3"
placeholder="Enter Your User ID"
value={userId}
onChange={(e) => setUserId(e.target.value)}
/>
<button
className="btn btn-primary"
onClick={joinChat}
disabled={!userId}
>
Join
</button>
</div>
);
}
return (
<div className="container py-5">
<h2 className="mb-4">Welcome {userId}!</h2>
<input
type="text"
className="form-control mb-3"
placeholder="Enter Receiver User ID"
value={receiverId}
onChange={(e) => setReceiverId(e.target.value)}
/>
<div className="border p-3 mb-3" style={{ height: "300px", overflowY: "auto" }}>
{messages.map((msg, index) => (
<div key={index} className="mb-2">
<b>{msg.userId}</b>: {msg.message}
</div>
))}
</div>
<div className="input-group">
<input
type="text"
className="form-control"
placeholder="Type a message"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
/>
<button className="btn btn-success" onClick={sendMessage}>
Send
</button>
</div>
</div>
);
}
Here, I have added just a simple example to show you the basic working of chat. There is no database connection right now. You can create REST API’s(using JAX-RS) for storing the messages, marking messages as read, and showing messages among users, showing unread messages and their count. Also, you can add proper CSS and JS for opening the chat in a drawer and handle it accordingly.
Important Note: Currently, I have added the whole code in app.jsx as the code is small and simple, for more scalability, it’s best practice to organize your code using components, utils, constants file. Create related reusable components, make use of Liferay’s client extension properties to filter data, create a separate file for calling the API, and structure the content properly.
Here, first of all user will be asked ID after entering their ID, and we can click on join. Then, we need to enter the receiverId. Then we are ready for a chat.
You can automate this based on the user and receiver accordingly
At last, your chat will look like the above image.
What you built:
A full working one-to-one chat application inside a Liferay client extension using Socket.IO and React.
What you learned:
- Setting up Socket.IO client and server
- Real-time event handling in React
- Building a clean UI using Bootstrap
Future Enhancements:
- Group Chat rooms
- Typing Indicators (“User is typing…”)
- Read Receipts (“Seen” messages)
- File Sharing (Images, Documents)



