03 oct 2025·6 min de lectura

Confusiones entre componentes de servidor y cliente en Next.js App Router: soluciones

Aprende a detectar mezclas entre componentes de servidor y cliente en Next.js App Router que causan crashes en tiempo de ejecución, y cómo reestructurar componentes, actions y la obtención de datos.

Confusiones entre componentes de servidor y cliente en Next.js App Router: soluciones

¿Qué es una mezcla de solo servidor vs solo cliente?\n\nUna mezcla de solo servidor vs solo cliente ocurre cuando el código se ejecuta en el lugar equivocado.\n\nEn Next.js App Router, algunos componentes están pensados para ejecutarse en el servidor (seguros para secretos, llamadas directas a la base de datos, APIs privadas). Otros están pensados para ejecutarse en el navegador (clicks de botones, estado local, acceso a window). Cuando esas responsabilidades se entrelazan, verás crashes, problemas de hidratación o builds que fallan solo después del despliegue.\n\nUn modelo mental útil:\n\n- Los Server Components obtienen y preparan datos, y luego pasan props simples hacia abajo.\n- Los Client Components manejan la interacción, pero no deberían traer código exclusivo del servidor.\n\nA menudo se ve bien en desarrollo porque el modo dev puede ser más tolerante. El hot reload, el empaquetado diferente y el timing pueden ocultar problemas de límites. Los builds de producción son más estrictos sobre lo que puede enviarse al navegador, y la hidratación tolera menos desacoples.\n\nSíntomas comunes:\n\n- Página en blanco tras la navegación (error solo en la consola)\n- Errores de hidratación donde la UI parpadea y luego falla\n- Errores 500 inesperados durante el render\n- “window is not defined” o “document is not defined”\n- Errores de build sobre importar módulos solo de servidor en archivos cliente\n\nEl código generado por IA hace esto más frecuente porque copia fragmentos sin respetar los límites. Un ejemplo típico: añadir "use client" a toda una página para arreglar un error de hook, aunque esa página importe un helper de base de datos o lea secretos.\n\n## Cómo App Router separa Server y Client Components\n\nEn App Router, cada componente es un Server Component por defecto. Esa única regla explica la mayoría de las sorpresas.\n\n### Server Components (por defecto)\n\nLos Server Components se ejecutan en el servidor. Úsalos para obtener datos, leer cookies/headers, usar secretos de entorno y cualquier trabajo pesado que no quieras en el navegador.\n\nSi toca tu base de datos, claves privadas de API o una sesión de auth, mantenlo en el servidor y pasa los resultados como props.\n\n### Client Components (opt-in)\n\nUn componente se convierte en Client Component solo cuando añades "use client" al inicio del archivo. Los Client Components se ejecutan en el navegador, así que pueden usar estado, effects, manejadores de eventos y APIs de navegador como localStorage.\n\nEl límite funciona así:\n\n- Un Server Component puede importar un Client Component. Todo lo que esté dentro de ese Client Component se ejecuta client-side.\n- Un Client Component no puede importar un Server Component ni módulos exclusivos del servidor.\n\nNormalmente necesitas "use client" cuando un componente usa hooks como useState/useEffect, eventos del navegador como onClick, o APIs del navegador como window y document.\n\nNo necesitas "use client" para UI que solo renderiza props. Una solución común (especialmente en prototipos generados por IA) es mantener la página y la carga de datos en el servidor, y renderizar un pequeño Client Component solo para la parte interactiva (un filtro, un modal o un editor inline).\n\n## Patrones de crash en tiempo de ejecución que puedes reconocer rápido\n\nLa mayoría de los crashes del App Router se reducen a un problema: código solo para navegador se ejecuta en el servidor, o código solo de servidor se empaqueta para el navegador.\n\n### Patrón 1: Hooks en un Server Component\n\nSi ves errores como “React Hook ... is not supported in Server Components” o “You're importing a component that needs useState/useEffect”, revisa el encabezado del archivo. Si el archivo no empieza con "use client", React lo considera un Server Component.\n\n### Patrón 2: Módulos solo de servidor importados en código cliente\n\nErrores que mencionan fs, path, crypto o “Module not found: Can't resolve 'fs'” suelen indicar que un componente cliente importó un helper compartido que (tal vez indirectamente) importa código exclusivo de Node.\n\nEsto ocurre con frecuencia cuando un archivo utils o lib compartido mezcla helpers para servidor y cliente, y el cliente lo importa “solo por una función”.\n\n### Patrón 3: APIs del navegador usadas durante el render en servidor\n\n“window is not defined”, “document is not defined” y “localStorage is not defined” significan que el código se está ejecutando en el servidor. Eso puede ser un Server Component, una Server Action, o incluso un módulo que se importa durante el render en servidor.\n\n### Patrón 4: Llamar lógica de servidor desde el cliente sin un puente seguro\n\nEstos aparecen como “You're importing a Server Action into a Client Component”, “Server-only module cannot be imported from a Client Component”, o una llamada client-side que accidentalmente ejecuta una función que nunca debió correr en el navegador.\n\n## Paso a paso: encuentra el límite malo en tu árbol de componentes\n\nLos wins más rápidos vienen de hacer el crash reproducible. Pruébalo en dev y luego en un build de producción. “Funciona en dev” no es excusa para problemas de límites.\n\nCuando leas el error, detente en el primer archivo que realmente sea tuyo. Los stack frames del framework son ruidosos. El primer archivo en tu repo es normalmente donde la importación o tipo de componente equivocado entra al árbol.\n\nUn flujo de trabajo simple:\n\n- Reproduce el crash siempre de la misma manera (la misma ruta, la misma acción, el mismo estado de usuario).\n- En el stack trace, salta al primer archivo de la app y anota qué componente lo renderizó.\n- Revisa el encabezado del archivo: ¿es un Server Component por defecto, o empieza con "use client"?\n- Sigue las importaciones hasta encontrar la primera discrepancia:\n - importación solo de servidor usada por código cliente (fs, clientes de DB, next/headers)\n - uso solo de navegador dentro de código de servidor (window, document, localStorage)\n- Decide la propiedad: secretos y datos en el servidor, estado UI y eventos en el cliente.\n\nUn fallo muy común: una página server pasa un cliente de base de datos, datos derivados de cookies o un helper solo de servidor a un componente cliente. Eso rompe. Haz fetch en el servidor y pasa JSON plano hacia abajo.\n\n## Reestructurando componentes que mezclan preocupaciones de servidor y cliente\n\nLos crashes ocurren cuando un componente intenta hacerlo todo.\n\nUna división fiable:\n\n- Server Component: obtiene datos, verifica auth, usa secretos.\n- Client Component: maneja estado, eventos, effects y cualquier UI dependiente del DOM.\n\nMueve el trabajo de datos hacia arriba en el árbol. Haz fetch en un Server Component (o en una función solo servidor llamada por él) y luego pasa el resultado como props sencillas. Esto evita que el código exclusivo del servidor llegue al bundle del navegador.\n\nLuego aísla la interactividad. Mantén las piezas cliente pequeñas para no enviar toda la página al navegador solo para que funcione un botón.\n\nEn el límite servidor→cliente, mantiene las props simples: strings, números, booleanos, arrays y objetos planos. No pases clientes de base de datos, objetos de request, instancias de clase ni funciones.\n\nEjemplo: una página de dashboard obtiene info del usuario, suscripción y actividad reciente, y además tiene filtros, un modal y un gráfico. Haz el fetch en DashboardPage (server) y pasa { userName, plan, activityItems } hacia abajo. Deja que un DashboardControls client component maneje el estado de los filtros y abrir/cerrar el modal.\n\n## Server Actions: patrones seguros para formularios y mutaciones\n\nLas Server Actions funcionan bien cuando un usuario envía un formulario y necesitas cambiar datos: crear un registro, actualizar un perfil, resetear una contraseña o ejecutar un pequeño workflow.\n\nUna estructura segura es mantener la UI del formulario en un Client Component y dejar la mutación en un archivo solo de servidor exportado como action. El cliente posee los inputs, el estado de carga y mostrar errores. El servidor posee las comprobaciones de autenticación, validación y llamadas a la DB.\n\nts\n// actions.ts\n'use server'\n\nexport async function updateProfile(formData: FormData) {\n const name = String(formData.get('name') ?? '')\n // validate, check auth, write to DB\n return { ok: true }\n}\n\n\nEn el lado cliente, pasa solo lo que el servidor necesita. No pases secretos, tokens o objetos de usuario crudos por props solo para que una action funcione. Si la action necesita saber quién es el usuario, léelo en el servidor (cookies/session) dentro de la action.\n\nDos hábitos previenen la mayoría de las fugas:\n\n- Valida inputs y vuelve a comprobar la autorización dentro de la action.\n- Devuelve errores seguros y amigables al usuario, no stack traces.\n\nSi quieres UI optimista, mantenla local y pequeña. Evita convertir una página entera en Client Component solo para mostrar un spinner.\n\n## Obtención de datos en App Router sin trabajo doble\n\nMuchos problemas de límite empiezan por hacer fetch en dos sitios.\n\nEn App Router, la opción por defecto debería ser fetch en el servidor cerca de la ruta. Obtendrás un primer pintado más rápido, bundles de navegador más pequeños y mantendrás secretos fuera del cliente.\n\nHaz fetch en el cliente solo cuando realmente lo necesites (polling, widgets en tiempo real o un botón de refrescar que actualiza solo una sección).\n\nUn bug común: el render en servidor obtiene datos, luego un Client Component monta y vuelve a obtener los mismos datos con useEffect. Eso puede causar parpadeos, límites de tasa y desajustes confusos.\n\nUn flujo limpio se ve así:\n\n- La petición llega a una ruta\n- El fetch en el servidor obtiene los datos (DB, API interna o tercero)\n- Los Server Components renderizan la página con esos datos\n- Los Client Components manejan interacciones y disparan actualizaciones puntuales\n\nEl caching también puede ocultar problemas durante las pruebas. Si los datos parecen aleatoriamente obsoletos, revisa si tu fetch está cacheado y si la revalidación está configurada como esperas.\n\n## Auth y secretos: qué debe quedarse en el servidor\n\nLos problemas de auth a menudo empiezan como un error de límites: un Client Component toca algo que nunca debería salir del servidor. A veces obtienes crashes o redirecciones extrañas. Otras veces filtras secretos silenciosamente al bundle cliente.\n\nLas filtraciones más comunes en código generado:\n\n- Leer variables de entorno en un Client Component\n- Poner configuración en un archivo compartido importado por servidor y cliente\n\nSi sería doloroso verlo en DevTools, no debería ser accesible desde código cliente.\n\nMantén las comprobaciones de autenticación y la lógica de roles en el servidor. El cliente puede renderizar estados de UI, pero no debería ser la fuente de la verdad para “¿tiene permiso este usuario?”.\n\nEvita almacenar tokens sensibles en localStorage por defecto. Es fácil de inspeccionar y puede ser robado por XSS.\n\nRupturas rápidas a vigilar:\n\n- Bucles de redirección cuando servidor y cliente intentan proteger la misma ruta\n- Desajustes de sesión donde el servidor renderiza un estado y el cliente se hidrata en otro\n- Confusión de runtime Edge vs Node para librerías de auth\n- “Funciona localmente, falla en prod” cuando las env vars difieren y los bundles cliente cambian\n\n## Errores comunes que hacen que los crashes vuelvan\n\nLa mayoría de los crashes repetidos no son bugs misteriosos del framework. Son los mismos errores de límites, parcheados a la rápida y luego reintroducidos.\n\nAlgunos patrones que se repiten:\n\n- Añadir "use client" a una página grande para silenciar un error de hook\n- Mantener helpers “compartidos” que mezclan código solo de servidor y seguro para cliente\n- Crear o importar un cliente de base de datos dentro de archivos de componentes (se propaga por las importaciones rápido)\n- Llamar fetch() a tu propia API route desde un Server Component por costumbre, aunque puedas llamar al código del servidor directamente\n- Arreglar por prueba y error en lugar de seguir la primera importación mala en el stack trace\n\nUn ejemplo típico: una página de dashboard falla solo en producción porque importa getUser() (lee cookies, solo servidor), pero la página tenía "use client" para soportar un gráfico. La solución durable es mover el gráfico a su propio Client Component y mantener la página server-first.\n\n## Lista rápida antes de enviar a producción\n\nLa mayoría de los crashes del App Router ocurren porque un archivo está haciendo dos trabajos.\n\n### Chequeo de límites\n\nPregúntate por cada componente: ¿puede este archivo ejecutarse en el navegador?\n\nSi la respuesta es sí, no debe tocar secretos, variables de entorno solo servidor, clientes de base de datos o librerías solo de Node. Si ves esas importaciones, mueve ese trabajo a un Server Component, una Server Action o una ruta de servidor.\n\nBarrido final:\n\n- APIs del navegador (window, document, localStorage, navigator) y hooks significan Client Component. Mantén la lógica de servidor fuera.\n- Secretos e importaciones solo de servidor significan Server Component. Pasa solo los datos que la UI necesita.\n- Props que crucen el límite deben ser serializables (objetos y arrays planos, strings, números). Evita instancias de clase, BigInt y funciones.\n- Para escrituras (formularios, updates, deletes), utiliza una Server Action o una ruta de servidor.\n- Prueba un build de producción localmente, no solo next dev.\n\n### Un hábito práctico\n\nAntes de enviar, recorre tus flujos principales después de un build limpio. Si una página falla solo en modo producción, normalmente es un problema de límites, una prop no serializable o una importación solo-de-servidor que se filtró al cliente.\n\n## Ejemplo: arreglando una página de dashboard que falla\n\nUna situación clásica: una página de dashboard necesita datos obtenidos en el servidor más filtros interactivos (rango de fechas, toggles de estado, búsqueda).\n\n### Qué salió mal\n\nLa primera versión suele mezclar responsabilidades en un solo archivo. Por ejemplo, app/dashboard/page.tsx obtiene datos del servidor, pero también usa useState, lee localStorage o llama a window.matchMedia para recordar ajustes de filtros. Eso funciona en el navegador, pero la página es un Server Component por defecto, así que puede fallar con “window is not defined” o “Hooks can only be used in a Client Component.”\n\nOtro desliz común: la UI de filtros está marcada con 'use client', pero importa un helper de servidor que lee cookies o accede a una base de datos privada. Eso puede disparar errores del estilo “You’re importing a Server Component into a Client Component”.\n\n### Una reestructuración simple que evita los crashes\n\nHaz que la página posea los datos y que el componente cliente posea la interactividad.\n\nEn el servidor (page): obtiene datos y los renderiza.\n\ntsx\n// app/dashboard/page.tsx (Server Component)\nimport Filters from './Filters';\nimport { getDashboardData } from './data';\n\nexport default async function Page() {\n const data = await getDashboardData();\n return (\n <>\n <Filters initial={data.filters} />\n {/* render table using data.items */}\n </>\n );\n}\n\n\nEn el cliente (filters): mantén el estado y los eventos UI locales y envía cambios mediante una Server Action.\n\ntsx\n// app/dashboard/actions.ts\n'use server';\nexport async function updateFilters(next) {\n // validate input, save, return safe data\n return { ok: true };\n}\n\n\nResultado: menos crashes en tiempo de ejecución, propiedad más clara (el servidor hace fetch y los secretos permanecen en el servidor; el cliente maneja clicks) y las actualizaciones siguen una ruta segura.\n\n## Siguientes pasos si tu código App Router sigue rompiéndose\n\nSi luchas con el mismo crash una y otra vez, normalmente es un problema de límites en todo el proyecto: código cliente jala módulos solo de servidor, código servidor importa hooks o las mutaciones quedan esparcidas por el cliente.\n\nLas bases de código generadas por IA de herramientas como Lovable, Bolt, v0, Cursor o Replit tienden a repetir estos errores porque mezclan patrones que funcionaban en setups antiguos y no resisten la separación más estricta del App Router.\n\nCuando los mismos síntomas aparecen en varias páginas, un refactor enfocado suele ser más rápido que parchear.\n\nSi heredaste un prototipo roto generado por IA y quieres un diagnóstico rápido y estructurado, FixMyMess (fixmymess.ai) se especializa en reparar y endurecer este tipo de codebases Next.js, empezando con una auditoría gratuita de código para identificar los primeros errores de límites e importaciones riesgosas.