Saltar al contenido principal
Intermedio Node.jsReactBackend

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.

Fran Cobos 5 min de lectura 831 palabras

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.

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.