Cómo solucionar "Text content does not match server-rendered HTML" en Next.js App Router
Este error ocurre cuando el HTML generado por el servidor (SSR/SSG) no coincide con lo que React intenta hidratar en el navegador durante la primera renderización del lado del cliente. En entornos de App Router, esto es especialmente común debido al uso de APIs del navegador o dependencias de tiempo en componentes que se renderizan en ambos lados.
Causa raíz (diagnóstico técnico)
La causa más probable en tu caso es el uso de Date() o APIs de tiempo en el renderizado, ya que el texto mostrado ("Latest Version16.3.7") sugiere que hay una fecha o versión dinámica renderizada directamente en el JSX. Otros candidatos comunes:
- Uso de
typeof window !== 'undefined'directamente en el render - Acceso a
localStorage,navigator,window.innerWidth, etc. - Componentes que dependen de
useEffectpero no se protegen durante SSR - Meta tags de detección automática de iOS (
format-detection) que inyectan nodos<a>en tiempo de ejecución
Solución definitiva (pasos verificados)
Paso 1: Identifica el elemento problemático
Busca en tus componentes de /app cualquier uso de:
-
new Date(),Date.now(),new Date().toLocaleString() -
localStorage.getItem(...),window.location, etc. -
Math.random(),crypto.randomUUID(), etc.
Paso 2: Aplica la solución correcta según el caso
Caso A: Contenido dinámico (fechas, versiones, UUIDs)
❌ Incorrecto:
export default function VersionInfo() {
return (
<div>
<p>Latest Version: {new Date().toISOString()}</p>
</div>
)
}
✅ Correcto (opción 1: usa suppressHydrationWarning para contenido inmutable):
export default function VersionInfo() {
return (
<div>
<p>Latest Version: <span suppressHydrationWarning>16.3.7</span></p>
</div>
)
}
⚠️ Si el texto realmente cambia (ej. versión dinámica), usa
dynamic({ ssr: false })ouseEffectpara cargarlo después del montaje.
Caso B: Uso de APIs del navegador
❌ Incorrecto:
export default function ClientOnlyComponent() {
const [data, setData] = useState(() => {
return localStorage.getItem('user') || 'guest'
})
return <div>{data}</div>
}
✅ Correcto (opción 2: deshabilitar SSR para el componente):
// components/ClientOnlyComponent.tsx
'use client'
import { useState, useEffect } from 'react'
export default function ClientOnlyComponent() {
const [data, setData] = useState('guest')
useEffect(() => {
const stored = localStorage.getItem('user')
if (stored) setData(stored)
}, [])
return <div>{data}</div>
}
✅ Correcto (opción 3: usar dynamic con ssr: false):
// page.tsx
import dynamic from 'next/dynamic'
const ClientOnlyComponent = dynamic(() => import('./ClientOnlyComponent'), {
ssr: false,
})
export default function Page() {
return <ClientOnlyComponent />
}
Paso 3: Prevenir problemas en iOS (si aplica)
Agrega este meta tag en tu <head> (en app/layout.tsx o app/layout.tsx con next/head):
// app/layout.tsx
import './globals.css'
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My App',
description: 'Generated by create next app',
viewport: 'width=device-width, initial-scale=1',
// 🔑 Solución crítica para iOS
other: {
'format-detection': 'telephone=no, date=no, email=no, address=no',
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
</head>
<body>{children}</body>
</html>
)
}
Pro-tip: Diagnóstico rápido con console.log en SSR
Agrega este helper temporalmente en tu componente sospechoso para identificar qué se renderiza en el servidor vs cliente:
export default function DebugHydration() {
if (typeof window === 'undefined') {
console.log('🚀 SERVER RENDER')
} else {
console.log('💻 CLIENT RENDER')
}
return <div>...</div>
}
Si los logs difieren en contenido (ej. servidor muestra "16.3.7", cliente muestra "16.3.7 (2024)"), ese es tu culpable.
✅ Recuerda: Nunca uses
Date()o APIs del navegador directamente en el render. UsauseEffecto deshabilita SSR para componentes que dependan de estado del cliente.
Resultado esperado: El error desaparecerá inmediatamente al eliminar la diferencia entre el HTML prerenderizado y el primero renderizado en cliente. Verifica con next dev y revisa la consola del navegador (no aparecerán warnings de hidratación).
Top comments (0)