Typing Indicators and Online Presence Enhancing Chat UX with Socket.io
Learn how to implement typing indicators and online/offline presence in your real-time chat using Socket.io for a polished, responsive chat experience.
Typing Indicators and Online Presence
Typing indicators and online status make chat feel real-time and engaging. This guide covers implementing both with Socket.io.
Typing Indicators
Server-Side
io.on("connection", (socket) => { // Start typing socket.on("typing:start", ({ targetUserId }) => { const roomId = getRoomId(socket.userId, targetUserId); // Broadcast to the room EXCEPT the sender socket.to(roomId).emit("typing:update", { userId: socket.userId, isTyping: true }); }); // Stop typing socket.on("typing:stop", ({ targetUserId }) => { const roomId = getRoomId(socket.userId, targetUserId); socket.to(roomId).emit("typing:update", { userId: socket.userId, isTyping: false }); }); });
Client-Side
const ChatWindow = ({ targetUserId }) => { const [isTyping, setIsTyping] = useState(false); const typingTimeoutRef = useRef(null); // Listen for typing events useEffect(() => { const handleTyping = ({ userId, isTyping }) => { if (userId === targetUserId) { setIsTyping(isTyping); } }; socket.on("typing:update", handleTyping); return () => socket.off("typing:update", handleTyping); }, [targetUserId]); // Emit typing events on input change const handleInputChange = (e) => { setNewMessage(e.target.value); // Start typing socket.emit("typing:start", { targetUserId }); // Clear previous timeout if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } // Stop typing after 3 seconds of no input typingTimeoutRef.current = setTimeout(() => { socket.emit("typing:stop", { targetUserId }); }, 3000); }; // Stop typing when message is sent const handleSend = () => { if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } socket.emit("typing:stop", { targetUserId }); // ... send message ... }; return ( <div> {/* Messages */} {isTyping && ( <div className="typing-indicator"> <span></span><span></span><span></span> {targetUser.firstName} is typing... </div> )} <input value={newMessage} onChange={handleInputChange} placeholder="Type a message..." /> </div> ); };
Online/Offline Presence
Server-Side
const onlineUsers = new Map(); io.on("connection", (socket) => { // User is online onlineUsers.set(socket.userId, socket.id); // Broadcast to all connected clients io.emit("user:online", { userId: socket.userId }); socket.on("disconnect", () => { onlineUsers.delete(socket.userId); io.emit("user:offline", { userId: socket.userId }); }); }); // API to check if a user is online const isUserOnline = (userId) => onlineUsers.has(userId); // Get all online users const getOnlineUsers = () => [...onlineUsers.keys()];
Client-Side
const ChatList = () => { const [onlineUsers, setOnlineUsers] = useState(new Set()); useEffect(() => { // Listen for online/offline events const handleOnline = ({ userId }) => { setOnlineUsers((prev) => new Set(prev).add(userId)); }; const handleOffline = ({ userId }) => { setOnlineUsers((prev) => { const newSet = new Set(prev); newSet.delete(userId); return newSet; }); }; socket.on("user:online", handleOnline); socket.on("user:offline", handleOffline); return () => { socket.off("user:online", handleOnline); socket.off("user:offline", handleOffline); }; }, []); return ( <div className="chat-list"> {conversations.map((conv) => ( <div key={conv._id} className="chat-item"> <div className="avatar"> <img src={conv.otherUser.photoUrl} alt="" /> {onlineUsers.has(conv.otherUser._id) && ( <span className="online-dot"></span> )} </div> <div className="info"> <h4>{conv.otherUser.firstName}</h4> <p>{conv.lastMessage?.text}</p> </div> {conv.unreadCount > 0 && ( <span className="unread-badge">{conv.unreadCount}</span> )} </div> ))} </div> ); };
Optimizing Presence
For large user bases, don't broadcast all online users to everyone. Instead:
// Only broadcast presence to connected users io.on("connection", (socket) => { onlineUsers.set(socket.userId, socket.id); // Find the user's connections const connections = await ConnectionRequest.find({ $or: [ { fromUserId: socket.userId, status: "accepted" }, { toUserId: socket.userId, status: "accepted" } ] }); // Notify only connected users connections.forEach((conn) => { const otherUserId = conn.fromUserId.equals(socket.userId) ? conn.toUserId : conn.fromUserId; io.to(otherUserId.toString()).emit("user:online", { userId: socket.userId }); }); });
The Takeaway
Typing indicators involve: emitting typing:start/typing:stop on input change, using a timeout to auto-stop after 3 seconds, and broadcasting to the room (excluding sender). Online presence involves: tracking online users in a Map, broadcasting user:online/user:offline events, showing a green dot in the chat list, and for large apps, only notifying connected users (not everyone) to reduce traffic.
On input change, emit typing:start. Set a timeout to emit typing:stop after 3 seconds of no input. Clear the timeout on each new keystroke and on send. The server broadcasts typing:update to the room (excluding sender). The receiver shows 'User is typing...' when isTyping is true.
Maintain a Map of online users (userId → socketId). On connection, add the user and broadcast user:online. On disconnect, remove and broadcast user:offline. The client listens for these events and shows a green dot for online users in the chat list.
Don't broadcast all online users to everyone. On connection, find the user's connections (accepted ConnectionRequests) and only notify those users about the online status. This reduces traffic significantly for large apps.
Emit typing:stop after 3 seconds of no input (using a timeout that's cleared and reset on each keystroke), when the message is sent, or when the user leaves the chat. This prevents the typing indicator from showing indefinitely.
No. For small apps, broadcasting to all is fine. For large apps, only notify connected users (people who have accepted connection requests with the user). This reduces unnecessary traffic and respects privacy strangers shouldn't see your online status.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

