Next.js 15 Hydration Mismatch: 'Text content did not match' Fixed

Imagen principal del artículo

Este error ocurre cuando el HTML generado en el servidor (SSR) no coincide con el que React intenta renderizar en el cliente, haciendo que la reconciliación falle en la hidratación. En Next.js 15 con el App Router, cualquier valor que cambie entre servidor y cliente —fechas, IDs aleatorios, API del navegador— dispara este warning que en producción puede degradar el UX.

El Problema

// ❌ Bad Code — app/page.tsx
export default function Page() {
  // Date() se evalúa en servidor Y en cliente en momentos distintos
  const timestamp = new Date().toLocaleTimeString();

  return (
    <main>
      <h2>Servidor renderizó: {timestamp}</h2>
    </main>
  );
}

Error en consola:

Warning: Text content did not match.
  Server: "20:05:10"
  Client: "20:05:11"

La Solución

// ✅ Good Code — app/page.tsx
'use client';

import { useState, useEffect } from 'react';

export default function Page() {
  const [timestamp, setTimestamp] = useState<string | null>(null);

  useEffect(() => {
    // Solo se ejecuta en el cliente, después de la hidratación
    setTimestamp(new Date().toLocaleTimeString());
  }, []);

  return (
    <main>
      {/* Renderiza null en SSR y el valor real solo en cliente */}
      <h2>Hora actual: {timestamp ?? 'Cargando...'}</h2>
    </main>
  );
}

Por qué funciona

El patrón useState(null) + useEffect garantiza que el valor dinámico nunca se incluye en el HTML del servidor. React hidrata el DOM estático (con null/placeholder), y solo después de que el cliente monta el componente actualiza el estado con el valor real. De este modo, el snapshot del servidor y del cliente son idénticos en el momento de la hidratación, eliminando el mismatch. En Next.js 15 esta es la estrategia canónica para cualquier valor dependiente de window, Date, Math.random() o APIs exclusivas del navegador.