Frontend nextjs

Qué puede (y no puede) cruzar de Server a Client en Next.js

Funciones cannot be passed directly to Client Components. Este error confunde hasta a devs con experiencia. Aquí la regla mental que lo resuelve de raíz.

10 min
Next.js React App Router Arquitectura

Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". Si trabajas con Next.js App Router y una librería de UI como MUI o Chakra, tarde o temprano te vas a topar con este error — y la mayoría de respuestas que vas a encontrar buscándolo son parches puntuales, no la explicación de fondo.

Este artículo es esa explicación de fondo, con dos casos reales donde me topé con el error construyendo un e-commerce con Next.js + MUI, y la regla mental que evita que vuelva a pasar.

El modelo mental correcto: no es “servidor vs cliente”, es “serializable vs no serializable”

App Router renderiza un árbol donde conviven Server Components (por defecto) y Client Components (marcados con 'use client'). Cuando un Server Component renderiza un Client Component y le pasa props, esos props tienen que viajar — literalmente se serializan a un formato que el cliente puede reconstruir (parecido a JSON, pero con soporte especial para elementos React).

La regla es simple una vez que la ves así: cualquier cosa que no se pueda serializar de esa forma, no puede cruzar como prop de Server a Client. Eso incluye:

  • Funciones “sueltas” (no marcadas 'use server')
  • Clases con métodos
  • Objetos con funciones anidadas dentro (como un theme de MUI, que trae alpha, lighten, darken como funciones)
  • Referencias directas a componentes usadas como valor de prop (no como elemento JSX renderizado)

Caso real #1: el theme de MUI

// ❌ layout.tsx (Server Component) — esto revienta
import { theme } from '@/styles/theme'
import { ThemeProvider } from '@mui/material/styles'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeProvider theme={theme}>{children}</ThemeProvider>
      </body>
    </html>
  )
}

El objeto theme se crea en el servidor y se intenta pasar como prop a ThemeProvider (un Client Component). Pero theme trae funciones internas (alpha, lighten, etc.) — no serializables. Error inmediato.

La solución no es “mover el theme al cliente a mano”, es encapsular su creación dentro de un Client Component propio, para que nunca cruce la frontera como prop:

// ✅ ThemeRegistry.tsx
'use client'

import { ThemeProvider } from '@mui/material/styles'
import { theme } from '@/styles/theme'

export default function ThemeRegistry({ children }: { children: React.ReactNode }) {
  return <ThemeProvider theme={theme}>{children}</ThemeProvider>
}
// ✅ layout.tsx — ya no toca nada de MUI directamente
import ThemeRegistry from '@/components/ThemeRegistry'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeRegistry>{children}</ThemeRegistry>
      </body>
    </html>
  )
}

layout.tsx sigue siendo Server Component (correcto, así debe ser el layout raíz), pero ahora solo le pasa children — que sí es serializable, es JSX.

Este es más sutil, y rompe incluso después de resolver el caso anterior. El patrón polimórfico de MUI (<Button component={Link} href="...">) parece inocente, pero:

// ❌ Desde un Server Component
import Link from 'next/link'
import Button from '@mui/material/Button'

export default async function CartPage() {
  return (
    <Button component={Link} href="/checkout">
      Ir a pagar
    </Button>
  )
}

Aquí no estás pasando un elemento JSX renderizado (<Link>...</Link>) — estás pasando la función Link en crudo como valor de la prop component. Es exactamente el mismo problema que el theme: una referencia a función cruzando la frontera como dato, no como elemento.

La solución oficial de MUI es un wrapper 'use client' para Link:

// ✅ src/components/atoms/Link.tsx
'use client'

import NextLink from 'next/link'
import { forwardRef } from 'react'
import type { ComponentProps } from 'react'

export const Link = forwardRef<HTMLAnchorElement, ComponentProps<typeof NextLink>>(
  function Link(props, ref) {
    return <NextLink ref={ref} {...props} />
  }
)

Ahora component={Link} referencia un Client Component ya reconocido por el bundler, no una función cruda — el bundler sabe cómo enlazarlo del servidor al cliente sin necesidad de serializarlo como dato.

Por qué el error a veces apunta al lugar equivocado

Un detalle que puede confundirte: cuando el stack trace del error apunta a una línea, a veces no es exactamente donde está el prop problemático. Next.js serializa el árbol completo del return de un Server Component de una sola pasada — si algo dentro de ese árbol no es serializable, el error se reporta sobre el nodo raíz que se estaba serializando en ese momento, no necesariamente sobre el prop exacto. Es como empacar una caja grande y que el error diga “la caja no cierra” en vez de señalar el objeto específico que no cabe. Cuando te pase, revisa todo el JSX de ese return, no solo la línea que marca el error.

La lista de verificación mental

Antes de pasar algo como prop de un Server Component a un Client Component, pregúntate:

  1. ¿Es JSX ya renderizado (<Componente />)? → Sí cruza.
  2. ¿Es un string, número, booleano, array/objeto plano de esos tipos? → Sí cruza.
  3. ¿Es una función suelta, una clase, o un objeto con métodos adentro? → No cruza — necesita un wrapper 'use client' que lo cree del lado del cliente.
  4. ¿Es una referencia a función/componente usada como valor de una prop (no como children)? → Mismo problema que el punto 3.

Conclusión

El error de serialización no es un capricho de Next.js — es la consecuencia directa de que Server y Client Components literalmente corren en procesos distintos, y todo lo que cruza entre ellos tiene que poder representarse como datos. Una vez que internalizas esa regla, dejas de “parchar” errores de MUI o de cualquier librería con Context Providers, y empiezas a diseñar tus wrappers 'use client' de forma proactiva.

¿Tu equipo está lidiando con errores de Server/Client Components en una migración a App Router? Agenda una asesoría técnica gratuita de 15 minutos y lo revisamos juntos.

¿Tienes un proyecto en mente?

Convierte tu idea en un producto real

Desarrollo web, aplicaciones a medida y consultoría tecnológica para empresas y startups. Cuéntame tu proyecto y te respondo en menos de 24 horas.

Solicitar presupuesto Ver LinkedIn