nextjs-server-client-components

Server Components en Next.JS

  • 6 min

Si venís de trabajar con Vite o Create React App, tenéis un modelo mental muy claro: Todo vuestro código de React se descarga en el navegador del usuario y se ejecuta allí.

El navegador descarga un archivo JS gigante (bundle), lo procesa, y React empieza a montar el DOM y a ejecutar los useEffect.

En Next.js, las reglas del juego cambian bastante. Con la llegada del App Router, Next.js adoptó los React Server Components (RSC).

Ahora, React puede ser “isomórfico” o “híbrido” (que palabars más bonitas) . Es decir, algunos componentes se ejecutan en vuestro servidor y nunca viajan al navegador, mientras que otros se comportan como siempre.

Entender esta distinción es una de las claves para trabajar bien con Next.js (y una de las cosas que más os liarán al principio… y bueno, también al final 😊).

Por defecto todo es Servidor

Esta es la regla número uno: En la carpeta app, todos los componentes son Server Components por defecto.

No tenéis que hacer nada especial. Si creáis un archivo page.tsx, ese código se ejecutará en el servidor durante el build (o en cada petición) y lo que se enviará al navegador será HTML puro.

Ventajas de los Server Components

  1. Cero JavaScript al Cliente: Si importáis una librería pesada (como una para formatear fechas o procesar Markdown) en un Server Component, esa librería se ejecuta en el servidor y no se añade al bundle que descarga el usuario.
  2. Seguridad y Acceso al Backend: Podéis acceder a la base de datos o usar claves API secretas directamente dentro del componente. Como el código nunca llega al navegador, es seguro.
  3. Latencia: Al estar en el servidor, el componente está físicamente cerca de vuestros datos.

Veamos un ejemplo,

// src/app/page.tsx
// Esto es un Server Component.
// Este console.log se verá en la TERMINAL de tu ordenador/servidor,
// NO en la consola del navegador.
import db from '@/lib/db'; 

export default async function Page() {
  console.log("Renderizando en servidor...");
  
  // Podemos acceder a BBDD directamente
  const datos = await db.query('SELECT * FROM users');

  return (
    <main>
      <h1>Lista de Usuarios</h1>
      {/* ... */}
    </main>
  );
}

Copied!

La directiva "use client"

Evidentemente, el HTML estático está muy bien, pero React se hizo famoso por la interactividad. ¿Cómo hacemos un botón que al hacer clic abra un menú? ¿Cómo usamos useState?

Los Server Components tienen limitaciones estrictas:

  • ❌ No pueden usar Hooks (useState, useEffect, useReducer…).
  • ❌ No pueden escuchar eventos del navegador (onClick, onChange, onSubmit).
  • ❌ No pueden acceder a APIs del navegador (window, localStorage).

Cuando necesitamos interactividad, debemos optar explícitamente por convertir el componente en un Client Component. Para ello, escribimos la directiva "use client" en la primera línea del archivo.

// src/components/Contador.tsx
'use client' // 👈 Marcamos este archivo como Client Component

import { useState } from 'react';

export default function Contador() {
  // Ahora sí podemos usar hooks y eventos
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicks: {count}
    </button>
  );
}

Copied!

Un “Client Component” no significa que solo se renderice en el cliente. Next.js también lo pre-renderiza en el servidor (SSR) para enviar el HTML inicial, y luego lo hidrata en el navegador para darle interactividad. Es el comportamiento “clásico” de React.

La frontera

El desafío con Next.js es decidir dónde poner la línea divisoria entre Servidor y Cliente.

Imaginad vuestro árbol de componentes. La raíz (layout.tsx) es Servidor. Vais bajando por las ramas. En el momento en que importáis un componente con 'use client', cruzáis la frontera.

Una vez cruzáis al lado del cliente, todos los componentes hijos importados dentro de ese archivo también serán considerados del lado del cliente (automáticamente, sin necesidad de poner la directiva).

La estrategia de las “Hojas del Árbol”

Para mantener el rendimiento al máximo, queremos que la mayor parte de nuestra app sea Server Components (estáticos y ligeros) y retrasar el uso de 'use client' lo máximo posible, llevándolo a las hojas (los extremos) del árbol.

Hacer que todo el layout.tsx sea 'use client' porque necesitáis un botón de login en el Navbar. Esto obligaría a descargar el JS de toda la aplicación.

Mantener el layout.tsx como Server Component, y aislar el botón en un componente pequeño <LoginButton /> que tenga 'use client', e importarlo en el layout.

El problema de la Composición

Hay una restricción más que os va a dar algún dolor de cabeza en algún momento (pero tiene sentido)

No podéis importar un Server Component dentro de un Client Component

Es decir, esto,

'use client'
// ❌ ERROR: Esto rompería porque ServerComponent tiene código de servidor (ej: db call)
// y el Client Component intenta meterlo en el bundle del navegador.
import ServerComponent from './ServerComponent';

export default function ClientWrapper() {
  return (
    <div>
      <ServerComponent />
    </div>
  );
}

Copied!

La solución es pasar el Server Component como children (prop).**

Si usamos el patrón de composición que vimos en el artículo de “Children y Slots”, podemos hacer esto:

// src/app/page.tsx (SERVER)
import ClientWrapper from './ClientWrapper';
import ServerComponent from './ServerComponent';

export default function Page() {
  return (
    // ✅ CORRECTO: El ClientWrapper recibe el ServerComponent ya renderizado como HTML.
    // No necesita importar su código fuente, solo pintar el "agujero" children.
    <ClientWrapper>
      <ServerComponent />
    </ClientWrapper>
  );
}

Copied!

¿Cuándo usar cuál?

Aquí tenéis la tabla de referencia que debéis tener en mente:

FuncionalidadServer Component (Por defecto)Client Component ('use client')
Acceso a Base de Datos✅ SÍ❌ NO (Inseguro)
Datos Confidenciales (Keys)✅ SÍ❌ NO (Se exponen)
Hooks (useState, useEffect)❌ NO✅ SÍ
Eventos (onClick, onChange)❌ NO✅ SÍ
APIs del Navegador (window)❌ NO✅ SÍ
Peso en el Bundle JS0 KB 🚀Depende del código