Build a Realtime Chat App with React, Node.js, and Socket.IO

by Didin J. on Aug 21, 2025 Build a Realtime Chat App with React, Node.js, and Socket.IO

Learn how to build a realtime chat app with React, Node.js, and Socket.IO. Step-by-step guide covering setup, UI, backend, and best practices.

Chat applications are among the most popular real-world use cases for real-time web technologies. From customer support tools to messaging platforms like Slack and WhatsApp, real-time communication has become an essential feature in modern applications.

In this tutorial, we’ll build a real-time chat application using React on the frontend, Node.js with Express on the backend, and Socket.IO for real-time, bidirectional communication between clients and the server.

By the end of this tutorial, you will have a fully functional chat app where multiple users can connect, send messages, and see updates instantly — all without refreshing the page. You’ll also gain practical experience in setting up and handling websockets, one of the core building blocks of interactive web applications.

Prerequisites

Before starting, ensure you have the following:

  • Node.js (v18 or higher) is installed on your machine. You can verify by running: 

    node -v
    npm -v

     

  • A basic understanding of JavaScript and React.

  • Familiarity with Node.js/Express fundamentals.

  • You’ll also need the following installed:

  • npm or yarn as a package manager.

  • A code editor like VS Code.

  • A modern browser (Chrome, Edge, or Firefox) for testing the app.

  •  
  • (Optional) Some knowledge of websockets will help, but we’ll cover everything as we go.

Project Setup

We’ll build this app with two separate projects:

  1. Backend → A Node.js + Express server with Socket.IO for real-time communication.

  2. Frontend → A React app to handle the user interface.

This separation keeps our project clean and scalable.


1. Create the Project Folder

Start by creating a root folder for your project:

mkdir realtime-chat-app
cd realtime-chat-app

Inside this folder, we’ll keep two subfolders:

  • server → for backend code

  • client → for frontend React app


2. Setup Backend (Node.js + Express + Socket.IO)

Navigate into the server folder:

mkdir server
cd server
npm init -y

Now, install the dependencies:

npm install express socket.io cors

Also, install nodemon for easier development (auto-restart):

npm install --save-dev nodemon

Your package.json scripts section should look like this:

"scripts": {
  "start": "node index.js",
  "dev": "nodemon index.js"
}

Create a new file index.js in the server folder:

// server/index.js
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",
    methods: ["GET", "POST"],
  },
});

io.on("connection", (socket) => {
  console.log(`User connected: ${socket.id}`);

  socket.on("send_message", (data) => {
    io.emit("receive_message", data);
  });

  socket.on("disconnect", () => {
    console.log("User disconnected", socket.id);
  });
});

server.listen(4000, () => {
  console.log("Server running on http://localhost:4000");
});

Run the backend server:

npm run dev

You should see:

Server running on http://localhost:4000


3. Setup Frontend (React with Vite)

From the project root (realtime-chat-app), create a new React app using Vite:

cd ..
npm create vite@latest client

You’ll be prompted with some options:

  • Project name: client

  • Framework: React

  • Variant: JavaScript (or TypeScript if you prefer)

Now move into the client folder and install dependencies:

cd client
npm install

Then, install the Socket.IO client library:

npm install socket.io-client

Start the frontend:

npm run dev

Build a Realtime Chat App with React, Node.js, and Socket.IO - vite + react

✅ At this point, we now have:

  • Backend (Node.js + Express + Socket.IO)http://localhost:4000

  • Frontend (React + Vite)http://localhost:5173

Both are ready to communicate in real-time.


4. Building the Chat UI (React with Vite + TypeScript)

Step 1: Update the Vite Project to TypeScript (if not already)

If you didn’t pick TypeScript when creating the Vite project, you can migrate manually. But since you selected it earlier, you should already have:

  • main.tsx instead of main.jsx

  • App.tsx instead of App.jsx

Step 2: Install Dependencies for TypeScript + Socket.IO

We already installed socket.io-client, but let’s add type definitions:

npm install socket.io-client
npm install -D @types/socket.io-client

Step 3: Set Up Socket.IO Client

Create a new file src/socket.ts:

import io from "socket.io-client";

const socket = io("http://localhost:4000");

export default socket;

This connects the frontend to our backend server at http://localhost:4000.

Step 4: Create the Chat UI

Edit src/App.tsx:

import { useEffect, useState } from "react";
import socket from "./socket";

function App() {
  const [message, setMessage] = useState("");
  const [chat, setChat] = useState<string[]>([]);

  useEffect(() => {
    // Listen for incoming messages
    socket.on("chat message", (msg: string) => {
      setChat((prev) => [...prev, msg]);
    });

    // Cleanup on unmount
    return () => {
      socket.off("chat message");
    };
  }, []);

  const sendMessage = (e: React.FormEvent) => {
    e.preventDefault();
    if (message.trim()) {
      socket.emit("chat message", message);
      setMessage("");
    }
  };

  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
      <h1 className="text-2xl font-bold mb-4">Realtime Chat App</h1>

      <div className="w-full max-w-md bg-white shadow-lg rounded-lg p-4">
        <div className="h-64 overflow-y-auto border rounded p-2 mb-4">
          {chat.map((msg, idx) => (
            <div key={idx} className="p-1 border-b">
              {msg}
            </div>
          ))}
        </div>

        <form onSubmit={sendMessage} className="flex">
          <input
            type="text"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            placeholder="Type a message..."
            className="flex-1 border rounded-l px-2 py-1"
          />
          <button
            type="submit"
            className="bg-blue-500 text-white px-4 rounded-r hover:bg-blue-600"
          >
            Send
          </button>
        </form>
      </div>
    </div>
  );
}

export default App;

Step 5: Add Tailwind CSS (Optional but Recommended for Styling)

If you want nicer styling, install Tailwind in your Vite project:

npm install tailwindcss @tailwindcss/vite postcss autoprefixer

Add the @tailwindcss/vite plugin to your Vite configuration.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss(),],
})

Update src/index.css:

@import "tailwindcss";

Now your chat UI will look much cleaner.

✅ With this, we now have a fully working Realtime Chat UI in React (TypeScript + Vite) that connects to our Node.js + Socket.IO backend.


5. Integrating Frontend and Backend (Realtime Communication)

At this point, you have:

  • A backend (server.js) running on port 4000, handling Socket.IO connections.

  • A frontend (React + Vite + TypeScript) running on port 5173, with a simple chat UI.

  • A socket.ts file to initialize the Socket.IO client.

Now, we’ll connect the two.

1. Update socket.ts (Frontend)

Ensure your socket client connects to the backend:

src/socket.ts

import { io } from "socket.io-client";

// If {io} import doesn’t work for you, replace with: import io from "socket.io-client";
const socket = io("http://localhost:4000");

export default socket;

2. Listen and Emit Events in React

We’ll hook into the chat UI so that:

  • When the user sends a message, it’s emitted to the server.

  • When the server broadcasts a message, all clients receive and update the UI.

Update your chat component:

src/components/Chat.tsx

import { useState, useEffect } from "react";
import socket from "../socket";

interface Message {
  user: string;
  text: string;
}

export default function Chat() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [username, setUsername] = useState("");

  useEffect(() => {
    // Listen for new messages
    socket.on("chat message", (msg: Message) => {
      setMessages((prev) => [...prev, msg]);
    });

    // Cleanup when component unmounts
    return () => {
      socket.off("chat message");
    };
  }, []);

  const sendMessage = () => {
    if (input.trim() && username.trim()) {
      const msg: Message = { user: username, text: input };
      socket.emit("chat message", msg);
      setInput("");
    }
  };

  return (
    <div className="flex flex-col h-screen p-4">
      {/* Username input */}
      {!username && (
        <div className="mb-4">
          <input
            type="text"
            placeholder="Enter your name..."
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            className="border p-2 rounded w-full"
          />
        </div>
      )}

      {/* Messages area */}
      <div className="flex-1 overflow-y-auto border p-4 rounded bg-gray-50">
        {messages.map((msg, i) => (
          <div key={i} className="mb-2">
            <strong>{msg.user}: </strong>
            <span>{msg.text}</span>
          </div>
        ))}
      </div>

      {/* Message input */}
      <div className="mt-4 flex gap-2">
        <input
          type="text"
          placeholder="Type a message..."
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="border p-2 rounded flex-1"
          disabled={!username}
          onKeyDown={(e) => e.key === "Enter" && sendMessage()}
        />
        <button
          onClick={sendMessage}
          className="bg-blue-600 text-yellow px-4 py-2 rounded disabled:opacity-50"
          disabled={!username}
        >
          Send
        </button>
      </div>
    </div>
  );
}

src/App.tsx

import React from "react";
import Chat from "./components/Chat";

const App: React.FC = () => {
  return (
    <div className="App">
      <Chat />
    </div>
  );
};

export default App;

3. Update the Backend to Broadcast Messages

In your index.js, ensure that incoming messages are broadcast to all connected clients:

index.js

// server/index.js
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:5173",
    methods: ["GET", "POST"]
  }
});

io.on("connection", socket => {
  console.log(`User connected: ${socket.id}`);

  socket.on("chat message", data => {
    console.log("Message received:", data);
    io.emit("chat message", data);
  });

  socket.on("disconnect", () => {
    console.log("User disconnected", socket.id);
  });
});

server.listen(4000, () => {
  console.log("Server running on http://localhost:4000");
});

4. Run Both Servers

Open two terminals:

Terminal 1 → Backend

npm run dev

Terminal 2 → Frontend

npm run dev

5. Test the Realtime Chat 🎉

  1. Open http://localhost:5173 in two browser windows.

  2. Enter different usernames.

  3. Send messages — you should see them appear instantly in both windows.

Build a Realtime Chat App with React, Node.js, and Socket.IO - chat

✅ At this point, you now have a fully working real-time chat app with React + Vite + TypeScript (frontend) and Node.js + Socket.IO (backend).


6. Best Practices for Realtime Apps with WebSockets

1. Efficient Event Naming

  • Use clear, descriptive event names (chat:message, user:joined, user:typing) instead of generic ones (message, event).

  • Helps scale and maintain as your app grows.

socket.emit("chat:message", { user, text });

2. Handle Connection & Reconnection Gracefully

  • Always listen for connect, disconnect, and reconnect events.

  • Show UI feedback when the connection is lost.

  • Optionally buffer unsent messages until reconnection.

socket.on("connect", () => console.log("Connected:", socket.id));
socket.on("disconnect", () => console.log("Disconnected from server"));

3. Validate and Sanitize Messages

  • Prevent XSS or malicious input from spreading across clients.

  • Escape HTML or strip unsafe tags before broadcasting messages.

import DOMPurify from "dompurify";
const cleanMsg = DOMPurify.sanitize(msg);

4. Use Rooms or Namespaces for Scalability

  • Don’t broadcast everything to everyone.

  • Group users by chat room, topic, or channel.

socket.join("room1");
io.to("room1").emit("chat:message", msg);

5. Error Handling

  • Add try/catch in backend event handlers.

  • Emit error events to the client if something goes wrong.

socket.on("chat:message", (msg) => {
  try {
    io.emit("chat:message", msg);
  } catch (err) {
    socket.emit("error", "Failed to send message");
  }
});

6. Authentication & Authorization

  • Require a valid JWT or session token before establishing a socket connection.

  • Use middleware to check tokens.

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (isValidToken(token)) {
    next();
  } else {
    next(new Error("Unauthorized"));
  }
});

7. Resource Management

  • Disconnect idle users if needed.

  • Remove event listeners on useEffect cleanup in React.

  • Monitor memory usage and socket count in production.

8. Monitoring & Logging

  • Log socket connections, disconnections, and errors.

  • Use tools like Prometheus + Grafana or Socket.IO Admin UI to track real-time metrics.

9. Deploy with Scaling in Mind

  • Socket.IO requires sticky sessions (so a client always talks to the same server instance).

  • Use Redis Adapter for scaling WebSockets across multiple servers.

import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";

const pubClient = createClient({ url: "redis://localhost:6379" });
const subClient = pubClient.duplicate();

io.adapter(createAdapter(pubClient, subClient));

✅ Following these practices ensures your realtime app is secure, scalable, and maintainable as it grows.


7. Conclusion + Next Steps

In this tutorial, we built a real-time chat application from scratch using React (with Vite + TypeScript) on the frontend, Node.js with Express on the backend, and Socket.IO to enable instant two-way communication.

You learned how to:

  • Set up a React frontend with Vite and TypeScript.

  • Create a simple chat UI with controlled input and a message list.

  • Build a Node.js + Express server and integrate Socket.IO for WebSocket communication.

  • Connect frontend and backend to send and receive messages in real-time.

  • Follow best practices for scalability, security, and reliability in real-time apps.

🚀 Next Steps

Now that you have a working real-time chat, here are some ideas to extend it further:

  1. Add User Authentication → Secure chat with JWT or OAuth, so users must log in before joining.

  2. Private & Group Chats → Use Socket.IO rooms or namespaces for private conversations and multiple channels.

  3. Typing Indicators & Read Receipts → Show when someone is typing or when a message has been read.

  4. Persist Chat History → Store messages in a database (MongoDB, PostgreSQL, or Redis) instead of memory.

  5. Deploy to Production → Use Docker or cloud platforms like Heroku, AWS, or Vercel. For scaling, add a Redis adapter for Socket.IO to handle multiple server instances.

  6. UI Enhancements → Add avatars, timestamps, message reactions, and infinite scroll for chat history.

By applying these improvements, you can evolve this project into a production-ready messaging platform.

You can find the full source code on our GitHub.

That's just the basics. If you need more deep learning about React.js, React Native, or related, you can take the following cheap course:

Thanks!