WebSockets con Socket.io en Node.js y React: Tutorial Real 2026
Implementa WebSockets en tiempo real con Socket.io 4 en Node.js y React. Chat, notificaciones, salas y autenticación con tokens JWT.
Tabla de contenidos
WebSockets es la tecnología detrás de cualquier feature en tiempo real: chats, notificaciones, dashboards live, juegos multijugador. Socket.io la simplifica enormemente.
Setup básico: servidor Node.js
mkdir ws-app && cd ws-app
npm init -y
npm install express socket.io cors
npm install -D typescript @types/node ts-node nodemon
Servidor
// server/index.ts
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
import cors from 'cors';
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: process.env.CLIENT_URL || 'http://localhost:5173',
methods: ['GET', 'POST'],
credentials: true,
},
});
// Middleware de autenticación
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Token requerido'));
}
try {
// Verifica el JWT
const payload = verificarToken(token);
socket.data.usuario = payload;
next();
} catch {
next(new Error('Token inválido'));
}
});
// Gestión de conexiones
io.on('connection', (socket) => {
const usuario = socket.data.usuario;
console.log(`Usuario conectado: ${usuario.nombre} (${socket.id})`);
// Unirse a una sala
socket.on('sala:unirse', (salaId: string) => {
socket.join(salaId);
socket.to(salaId).emit('sala:usuario-unido', {
usuario: usuario.nombre,
timestamp: new Date().toISOString(),
});
console.log(`${usuario.nombre} se unió a la sala ${salaId}`);
});
// Enviar mensaje a una sala
socket.on('mensaje:enviar', (data: { salaId: string; texto: string }) => {
const mensaje = {
id: crypto.randomUUID(),
texto: data.texto,
autor: usuario.nombre,
autorId: usuario.id,
timestamp: new Date().toISOString(),
};
// Emite a todos en la sala (incluyendo el emisor)
io.to(data.salaId).emit('mensaje:nuevo', mensaje);
});
// Notificar escritura
socket.on('mensaje:escribiendo', (salaId: string) => {
socket.to(salaId).emit('mensaje:usuario-escribiendo', usuario.nombre);
});
// Desconexión
socket.on('disconnect', (reason) => {
console.log(`${usuario.nombre} desconectado: ${reason}`);
});
});
httpServer.listen(3001, () => {
console.log('Servidor WebSocket en puerto 3001');
});
Cliente React
npm install socket.io-client
Hook personalizado para Socket.io
// hooks/useSocket.ts
import { useEffect, useRef, useState } from 'react';
import { io, Socket } from 'socket.io-client';
interface UseSocketOptions {
token: string;
serverUrl?: string;
}
export function useSocket({ token, serverUrl = 'http://localhost:3001' }: UseSocketOptions) {
const socketRef = useRef<Socket | null>(null);
const [conectado, setConectado] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const socket = io(serverUrl, {
auth: { token },
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
});
socket.on('connect', () => {
setConectado(true);
setError(null);
});
socket.on('disconnect', () => setConectado(false));
socket.on('connect_error', (err) => {
setError(err.message);
setConectado(false);
});
socketRef.current = socket;
return () => {
socket.disconnect();
};
}, [token, serverUrl]);
return { socket: socketRef.current, conectado, error };
}
Componente de chat
// components/Chat.tsx
import { useState, useEffect, useRef } from 'react';
import { useSocket } from '../hooks/useSocket';
interface Mensaje {
id: string;
texto: string;
autor: string;
autorId: string;
timestamp: string;
}
interface ChatProps {
salaId: string;
token: string;
usuarioId: string;
}
export function Chat({ salaId, token, usuarioId }: ChatProps) {
const { socket, conectado } = useSocket({ token });
const [mensajes, setMensajes] = useState<Mensaje[]>([]);
const [input, setInput] = useState('');
const [escribiendo, setEscribiendo] = useState<string | null>(null);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!socket) return;
// Unirse a la sala al montar
socket.emit('sala:unirse', salaId);
// Escuchar mensajes nuevos
socket.on('mensaje:nuevo', (mensaje: Mensaje) => {
setMensajes(prev => [...prev, mensaje]);
});
// Indicador de escritura
socket.on('mensaje:usuario-escribiendo', (nombre: string) => {
setEscribiendo(nombre);
setTimeout(() => setEscribiendo(null), 2000);
});
return () => {
socket.off('mensaje:nuevo');
socket.off('mensaje:usuario-escribiendo');
};
}, [socket, salaId]);
// Scroll al último mensaje
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [mensajes]);
function enviar() {
if (!input.trim() || !socket) return;
socket.emit('mensaje:enviar', { salaId, texto: input });
setInput('');
}
function handleInput(e: React.ChangeEvent<HTMLInputElement>) {
setInput(e.target.value);
socket?.emit('mensaje:escribiendo', salaId);
}
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{mensajes.map(m => (
<div
key={m.id}
className={`flex ${m.autorId === usuarioId ? 'justify-end' : 'justify-start'}`}
>
<div className={`max-w-xs px-3 py-2 rounded-lg text-sm ${
m.autorId === usuarioId
? 'bg-blue-500 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
}`}>
{m.autorId !== usuarioId && (
<p className="text-xs font-semibold mb-1 opacity-70">{m.autor}</p>
)}
<p>{m.texto}</p>
</div>
</div>
))}
{escribiendo && (
<p className="text-xs text-gray-400 italic">{escribiendo} está escribiendo...</p>
)}
<div ref={bottomRef} />
</div>
<div className="p-3 border-t dark:border-gray-700 flex gap-2">
<input
value={input}
onChange={handleInput}
onKeyDown={e => e.key === 'Enter' && enviar()}
placeholder="Escribe un mensaje..."
className="flex-1 border rounded-lg px-3 py-2 text-sm dark:bg-gray-800 dark:border-gray-600"
disabled={!conectado}
/>
<button
onClick={enviar}
disabled={!conectado || !input.trim()}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm disabled:opacity-50"
>
Enviar
</button>
</div>
</div>
);
}
Escalar con Redis (múltiples servidores)
Si tienes más de una instancia del servidor, los sockets de diferentes instancias no se comunican. La solución es el adaptador de Redis:
npm install @socket.io/redis-adapter redis
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
Problemas comunes
Socket.io con Nginx: añade estas cabeceras en tu config de proxy:
location /socket.io/ {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
CORS en producción: especifica los orígenes exactos, nunca origin: '*' en producción. Puedes ver la guía completa de CORS en Node.js.
Para la autenticación con JWT en el middleware de Socket.io, aplica los mismos principios que en proteger una API Node.js con JWT.