TechLead

Componentes Servidor y Cliente

Entiende React Server Components y cuándo usar componentes cliente

React Server Components (RSC)

Next.js usa React Server Components por defecto en el App Router. Los Server Components se renderizan en el servidor, reducen el JavaScript del lado del cliente y pueden acceder directamente a recursos backend como bases de datos y sistemas de archivos.

🖥️ Componentes Servidor (Por Defecto)

  • ✅ Obtener datos directamente (sin useEffect)
  • ✅ Acceder a recursos backend (BD, filesystem)
  • ✅ Mantener información sensible en el servidor (API keys)
  • ✅ Reducir tamaño del bundle del cliente
  • ❌ No pueden usar hooks (useState, useEffect)
  • ❌ No pueden usar APIs del navegador
  • ❌ No pueden agregar event listeners

💻 Componentes Cliente ('use client')

  • ✅ Usar hooks de React (useState, useEffect)
  • ✅ Agregar event listeners (onClick, onChange)
  • ✅ Acceder a APIs del navegador (localStorage, window)
  • ✅ Usar librerías de hooks de terceros
  • ❌ No pueden ser funciones async
  • ❌ Aumentan el tamaño del bundle del cliente

Ejemplo de Componente Servidor

Los Server Components pueden ser async, obtener datos directamente y mantener secretos en el servidor mientras retornan solo HTML al cliente.

// app/users/page.tsx - Componente Servidor (por defecto)
// No se necesita directiva 'use client'

import { db } from '@/lib/database';

// Este componente se ejecuta solo en el servidor
export default async function UsersPage() {
  // Acceso directo a base de datos - ¡no se necesita API!
  const users = await db.user.findMany();
  
  // Async/await funciona directamente en el componente
  const stats = await fetch('https://api.example.com/stats', {
    headers: {
      // Seguro de usar - nunca se envía al cliente
      Authorization: `Bearer ${process.env.SECRET_API_KEY}`,
    },
  }).then(res => res.json());

  return (
    <div>
      <h1>Usuarios ({stats.total})</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

// ¡Cero JavaScript del lado del cliente para este componente!

Ejemplo de Componente Cliente

Los Client Components optan por APIs del navegador y hooks, por lo que son ideales para UI interactiva y comportamiento con estado.

'use client'; // Esta directiva lo hace un Componente Cliente

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  const [mounted, setMounted] = useState(false);

  // useEffect funciona en Componentes Cliente
  useEffect(() => {
    setMounted(true);
    // Acceder a APIs del navegador
    const saved = localStorage.getItem('count');
    if (saved) setCount(parseInt(saved));
  }, []);

  const increment = () => {
    setCount(c => c + 1);
    localStorage.setItem('count', String(count + 1));
  };

  if (!mounted) return null; // Evitar desajuste de hidratación

  return (
    <button onClick={increment}>
      Contador: {count}
    </button>
  );
}

Patrones de Composición

Prefiere envolver Server Components con un pequeño Client Component para mantener la mayoría del árbol renderizado en el servidor y evitar inflar el bundle del cliente.

// ✅ CORRECTO: Pasar Server Components como children
// app/dashboard/page.tsx (Componente Servidor)
import ClientWrapper from './ClientWrapper';
import ServerData from './ServerData';

export default async function Dashboard() {
  return (
    <ClientWrapper>
      {/* ServerData permanece como Server Component */}
      <ServerData />
    </ClientWrapper>
  );
}

// ClientWrapper.tsx
'use client';
export default function ClientWrapper({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && children}
    </div>
  );
}

// ❌ INCORRECTO: Importar Server Component en Client Component
'use client';
import ServerComponent from './ServerComponent'; // ¡Esto se convierte en Client Component!

Cuándo Usar Cada Uno

Caso de Uso Tipo de Componente
Obtener datos 🖥️ Servidor
Acceder a recursos backend 🖥️ Servidor
Mantener datos sensibles seguros 🖥️ Servidor
Agregar interactividad (onClick, onChange) 💻 Cliente
Usar estado (useState, useReducer) 💻 Cliente
Usar efectos de ciclo de vida (useEffect) 💻 Cliente
Usar APIs solo del navegador 💻 Cliente
Usar React Context 💻 Cliente

Mezclar Componentes

Mantén la interactividad del cliente en las hojas para que los componentes pesados en datos permanezcan renderizados en el servidor.

// Mejor Práctica: Mantener Componentes Cliente en las hojas

// ❌ MAL: Cliente Component grande envolviendo todo
'use client';
export default function Page() {
  const [filter, setFilter] = useState('');
  return (
    <div>
      <input onChange={e => setFilter(e.target.value)} />
      <DataTable filter={filter} /> {/* Ahora forzado a ser cliente */}
    </div>
  );
}

// ✅ BIEN: Pequeño Client Component solo para interactividad
// SearchFilter.tsx
'use client';
export function SearchFilter({ onFilter }: { onFilter: (v: string) => void }) {
  return <input onChange={e => onFilter(e.target.value)} />;
}

// page.tsx (Componente Servidor)
import { SearchFilter } from './SearchFilter';
import { DataTable } from './DataTable'; // Permanece Server Component

export default async function Page() {
  const data = await fetchData();
  return (
    <div>
      <SearchFilter onFilter={handleFilter} />
      <DataTable data={data} />
    </div>
  );
}

Librerías de Terceros

Muchas librerías de UI dependen de hooks, así que envuélvelas en un pequeño Client Component y pasa datos desde el servidor.

// Muchas librerías usan hooks y necesitan 'use client'
// Crear componentes wrapper para componentes de terceros

// components/ChartWrapper.tsx
'use client';

import { LineChart } from 'some-chart-library';

export function ChartWrapper({ data }: { data: number[] }) {
  return <LineChart data={data} />;
}

// Usar en Componente Servidor
import { ChartWrapper } from './ChartWrapper';

export default async function Dashboard() {
  const data = await fetchChartData(); // Obtención del lado del servidor
  return <ChartWrapper data={data} />; // Renderizado del lado del cliente
}

📖 Documentación de Server Components →

⚡ Puntos Clave

  • • Por defecto usa Server Components - solo usa 'use client' cuando sea necesario
  • • Mantén los Client Components pequeños y en los nodos hoja
  • • Pasa Server Components como children a Client Components
  • • No importes Server Components en Client Components
  • • Envuelve librerías de terceros que necesiten hooks