Skip to content

Real-Time Chat

Chat in rooms with a Socket.IO server that relays messages and a React client that keeps login, room and messages in Redux Toolkit.

Stage 4advanced4 min read

Problem Statement

Build a small chat app where people in the same room see each other's messages instantly. There is no database. A Node server only relays messages between browsers that are connected at the same time.

Functional Requirements

  1. Join with a name: The user types a username and joins. Empty names are ignored.
  2. Rooms: Everyone starts in general. The user can switch to any other room by name. Switching clears the message list.
  3. Send and receive: A message goes to everyone in the current room, including the sender, and appears at once.
  4. Connection status: The header shows "Connected" or "Connecting…". Sending is disabled while offline.
  5. Leave: A Leave button logs out and closes the socket.

Technical Constraints

  • React with TypeScript, Redux Toolkit for state, styled-components for styles.
  • Socket.IO on both sides. The server keeps no history.
  • The client reads the server URL from VITE_SOCKET_URL and falls back to http://localhost:4000.

How the pieces talk

Loading diagram…
  1. Login dispatches loginSuccess after a fake 500 ms delay. A real app would call an API here.
  2. App watches isAuthenticated. When it turns true it opens the socket. Its cleanup closes the socket on logout or unmount.
  3. ChatRoom joins a room as soon as the socket reports connect, and again after every reconnect.
  4. The server sends each message back to the whole room with io.to(room).emit, so the sender sees their own message the same way everyone else does.

Implementation Notes & Key Concepts

1. Keep the socket outside React

The socket lives in a module variable in socketService.ts, not in state. It is not something to render, and putting it in Redux would break serialisation. Components call plain functions (joinRoom, sendMessage). Incoming events become Redux actions, so components only read from the store.

socket.on("receive_message", (message: Message) => dispatch(addMessage(message)));

initializeSocket returns early if a socket already exists. disconnectSocket removes the listeners and sets the variable back to null. Without that, React StrictMode (which runs effects twice in development) would leave two sockets that each dispatch every message.

2. One message type for both directions

Message is defined once in chatSlice.ts, and the service imports it. The timestamp is a number (Date.now()), so the client formats it with toLocaleTimeString and nothing has to parse strings.

3. Reducers that defend themselves

addMessage ignores a message for a room the user has left, and one whose id is already in the list. Both can happen around reconnects.

if (message.room !== state.currentRoom) return;
if (state.messages.some((existing) => existing.id === message.id)) return;

4. The server checks what it relays

  • join_room leaves every other room first, so a user is in one room at a time.
  • send_message checks the shape and length of the message, and only relays it to a room the sender has joined. Without that check, any client could post into any room.
  • CORS allows only CLIENT_ORIGIN (default http://localhost:5173).

5. Why port 4000 and not 6000

Chrome and Firefox refuse to connect to port 6000 (it is reserved for X11 and is on their blocked-ports list). The first version used 6000, so the client could never connect. The server now reads PORT and defaults to 4000.


Code Walkthrough

backend/server.js

  • Express app, HTTP server and a Socket.IO server with CORS for the Vite dev origin.
  • isValidMessage(data): shape and length checks before a message is relayed.
  • join_room, send_message and disconnect handlers as described above.

frontend/src/services/socketService.ts

  • initializeSocket(dispatch): connects once and maps connect, disconnect and receive_message to actions.
  • joinRoom, sendMessage, disconnectSocket: thin wrappers over socket.emit and socket.disconnect.

frontend/src/store/

  • authSlice.ts: loginStart, loginSuccess, loginFailure, logout.
  • chatSlice.ts: messages, currentRoom and isConnected, with the guards in addMessage.
  • index.ts: configureStore plus typed useAppDispatch and useAppSelector hooks.

frontend/src/components/

  • Login.tsx: a controlled input. The user id comes from crypto.randomUUID(), so no extra package is needed.
  • ChatRoom.tsx: header with room and status, a form to switch rooms, the message list (scrolls to the newest message), and the send form.

Running it

cd backend && npm install && npm run dev      # http://localhost:4000
cd frontend && npm install && npm run dev     # http://localhost:5173

Open the client in two windows, join with different names, and send a message.

Interview follow-ups

  1. How would you keep message history? Store messages per room in a database such as Postgres or MongoDB, and send the last 50 when a user joins the room.
  2. How would you show that someone is typing? Emit a typing event with a short debounce, and clear the indicator after two seconds without one.
  3. How do you keep messages in order and confirm delivery? Stamp each message on the server and sort by that time. Use Socket.IO acks (socket.emit(event, data, callback)) to mark a message as sent.
  4. What changes when you run several Node servers? Each server only knows its own sockets, so add the Socket.IO Redis adapter to pass room messages between servers.

In this topic

Done with this topic?

How well do you know it?

PreviousTrello Board
Practise 4 questions