Inicio / Artículos / Dónde trazar la línea servidor-cliente en una página del App Router de Next.js

Dónde trazar la línea servidor-cliente en una página del App Router de Next.js

Un modelo mental práctico para los Componentes Servidor de React: qué se ejecuta dónde, cómo una página de artículo de blog se divide en partes del servidor y del cliente, y las reglas de importación a seguir.

1321 palabras

Los Componentes del Servidor de React suelen resultar complicados incluso después de leer la documentación, sobre todo porque exigen que se abandone una suposición que todo desarrollador de React ha mantenido durante años: que los componentes se ejecutan en el navegador. Una vez desechada esa suposición, lo demás sigue de manera bastante natural. Este artículo construye un modelo mental basado en una única página de blog, mostrando qué partes deben estar en el servidor, cuáles necesitan el cliente y las pocas reglas que mantienen limpia la separación entre ellos.

Para conocer en profundidad el propio proceso de renderizado, consulte la arquitectura detrás del renderizado sin bundle con Componentes del Servidor.

De “todo se hidrata” a “solo lo necesario”

Antes de los Componentes del Servidor, cada componente de React se ejecutaba eventualmente en el navegador. Incluso con la renderización del lado del servidor, todo el código JavaScript relacionado con el árbol de componentes se enviaba al cliente y se cargaba para que la aplicación pudiera volverse interactiva. Esto es un desperdicio para componentes que solo obtienen datos y los transmiten como propiedades: su código se descarga y ejecuta sin nunca reaccionar al usuario.

Un Componente del Servidor cambia esto al ejecutarse únicamente en el servidor. Su código nunca se envía al navegador ni se carga, por lo que no agrega JavaScript al paquete del cliente. Lo que llega al navegador es su salida renderizada, la cual React serializa en una carga útil compacta junto con el HTML.

La forma más sencilla de diferenciarlos es:

  • Componente del Cliente: se ejecuta en el navegador (después de ser pre-renderizado en el servidor), puede almacenar estado y responder a eventos.
  • Componente del servidor: se ejecuta en el servidor, puede leer datos directamente de bases de datos o archivos, y solo envía la salida procesada.
  • Por qué esta distinción es beneficiosa

    Tomemos una página típica de entrada de blog. El título y el cuerpo provienen de una base de datos, se ven iguales para todos los visitantes e ignoran los clics. Tradicionalmente, el código que los procesa, junto con cualquier biblioteca de formato Markdown, sigue siendo enviado al navegador. Con los componentes del servidor, todo eso permanece en el servidor. El tamaño del paquete disminuye y las páginas suelen cargarse más rápido, especialmente en dispositivos lentos.

    Los equipos que pasan al App Router, construido sobre componentes del servidor, han reportado mejores valores de Core Web Vitals, en particular LCP, en algunos escenarios. Considérelos dependientes del contexto: la mejora depende de cuánto JavaScript del cliente eliminan sus páginas, así que mida sus propias rutas.

    Una página de entrada de blog, dividida en dos

    En un proyecto de Next.js App Router, un archivo de página es un componente del servidor a menos que se indique lo contrario. La página siguiente es una función async que espera la respuesta directamente desde la capa de datos, maneja el caso de elemento no encontrado, renderiza el contenido estático y luego agrega un único componente interactivo para los comentarios. Está escrita según la API de Next.js 14:

    // app/posts/[slug]/page.tsx
    // This is a Server Component by default — no "use client" needed
    
    import { getPostBySlug } from "@/lib/db"
    import { PostContent } from "@/components/PostContent"
    import { CommentSection } from "@/components/CommentSection"
    
    type Props = {
      params: { slug: string }
    }
    
    export default async function PostPage({ params }: Props) {
      // Direct DB call — no useEffect, no API route, no loading state
      const post = await getPostBySlug(params.slug)
    
      if (!post) {
        return <div>Post not found.</div>
      }
    
      return (
        <article className="max-w-2xl mx-auto py-12 px-4">
          <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
          <PostContent content={post.body} />
    
          {/* This one needs interactivity — so it's a Client Component */}
          <CommentSection postId={post.id} />
        </article>
      )
    }
    

    Observe lo que falta: no hay useState, ni useEffect, ni envoltorio de ruta API, ni registro del estado de carga. Los datos se esperan tal como ocurre en cualquier otra función async, lo cual constituye la esencia del modelo.

    Dos notas prácticas. A partir de Next.js 15, params se pasa como una Promise, por lo que la página debería await params antes de leer slug; verifica la versión que estás utilizando. Y en caso real de página no encontrada, al llamar a notFound() desde next/navigation se devuelve un estado 404 adecuado en lugar de una página normal con un mensaje de error.

    El cuadro de comentarios es diferente. Mantiene el texto en borrador en el estado y reacciona a las escrituras y clics, por lo que debe ejecutarse en el navegador. La directiva "use client" al principio del archivo lo marca como un Componente Cliente:

    // components/CommentSection.tsx
    "use client" // opts into browser rendering
    
    import { useState } from "react"
    
    type Props = {
      postId: string
    }
    
    export function CommentSection({ postId }: Props) {
      const [comment, setComment] = useState("")
    
      const handleSubmit = async () => {
        await fetch("/api/comments", {
          method: "POST",
          body: JSON.stringify({ postId, comment }),
        })
        setComment("")
      }
    
      return (
        <div className="mt-8">
          <textarea
            value={comment}
            onChange={(e) => setComment(e.target.value)}
            placeholder="Leave a comment..."
            className="w-full border rounded p-2 text-sm"
          />
          <button
            onClick={handleSubmit}
            className="mt-2 bg-blue-600 text-white px-4 py-2 rounded text-sm"
          >
            Post Comment
          </button>
        </div>
      )
    }
    

    Todo lo interactivo se encuentra aquí: el estado local del área de texto, un manejador de cambios y un manejador de envío que envía los datos a una ruta API y luego limpia el campo. Al implementarlo en la práctica, envíe un encabezado Content-Type: application/json con la solicitud, y considere utilizar una Acción del Servidor como alternativa a una ruta API separada.

    Esta división resultante es fácil de comprender: el servidor gestiona los datos, mientras que el cliente se encarga de la interacción.

    Reglas para mantener los límites claros

    • En App Router, los componentes son Componentes del Servidor por defecto.
    • Agregue "use client" solo donde necesite estado, efectos, manejadores de eventos o APIs del navegador como window y localStorage.
    • Los Componentes del Servidor pueden importar y renderizar Componentes del Cliente, tal como lo hace la página con CommentSection.
  • Los componentes del cliente no pueden importar componentes del servidor, pero sí pueden recibirlos como hijos u otros atributos, lo que permite anidar contenido renderizado por el servidor dentro de una interfaz interactiva.
  • Los atributos pasados desde el servidor al cliente deben ser serializables: los datos simples funcionan, pero las funciones e instancias de clases no.
  • El estilo no se ve afectado. Las clases de Tailwind y el CSS funcionan de la misma manera en ambos tipos de componentes.
  • "use client" también marca un límite en lugar de un único archivo: todo lo que ese archivo importe pasa a formar parte del paquete del cliente. Al colocar la directiva lo más abajo posible en la estructura, en las hojas interactivas pequeñas, se mantiene el resto en el servidor.

    Conclusión

    El modelo mental se aclara en cuanto “¿dónde se ejecuta esto?” se convierte en la primera pregunta que se hace sobre un componente, y la respuesta por defecto del App Router es el servidor.

    • Los componentes de servidor trasladan el procesamiento de la renderización y el acceso a los datos al servidor, por lo que no incluyen JavaScript de componentes.
    • La obtención de datos se realiza mediante async/await dentro del componente.
    • Considere "use client" como una opción para elementos interactivos, y no como la configuración predeterminada.
    • Un primer paso práctico: elija un componente de obtención de datos en un proyecto existente, averigüe si realmente necesita el navegador y conviértalo si no es necesario.

    Con este modelo establecido, los diseños, la transmisión por Suspense, las acciones de servidor y las rutas paralelas se vuelven mucho más fáciles de aprender, ya que todos se basan en la misma filosofía centrada en el servidor.

    Lecturas relacionadas

  • Migrar de next/router: Params, URLs Shallow y 404s en App Router — Descubre cómo funcionan los params, useParams, las actualizaciones de URLs Shallow, la navegación programática y las páginas 404 reales en el Next.js App Router una vez que next/router ya no esté disponible.
  • SEO técnico en el Next.js App Router: Metadatos, sitemaps y JSON-LD — Aprende cómo los ayudantes de metadatos compartidos, los valores por defecto del layout raíz, robots.ts, un sitemap dinámico, JSON-LD preciso y las auditorías de páginas proporcionan a una aplicación Next.js una base sólida para el SEO.
  • Por qué WebSockets se atascan en las rutas API de Next.js y cómo un servidor personalizado lo soluciona — Aprenda por qué un servidor WS dentro de una ruta pages/api nunca completa el intercambio de datos, y cómo manejar el evento de actualización por su cuenta con un servidor personalizado de Next.js.