Patrones de Composición para Servidor y Cliente
Al construir aplicaciones React, deberás considerar qué partes de tu aplicación deben renderizarse en el servidor o en el cliente. Esta página cubre algunos patrones de composición recomendados al usar Componentes de Servidor y Cliente.
¿Cuándo usar Componentes de Servidor y Cliente?
Aquí hay un resumen rápido de los diferentes casos de uso para Componentes de Servidor y Cliente:
¿Qué necesitas hacer? | Componente Servidor | Componente Cliente |
---|---|---|
Obtener datos | ||
Acceder a recursos del backend (directamente) | ||
Mantener información sensible en el servidor (tokens de acceso, claves API, etc) | ||
Mantener dependencias grandes en el servidor / Reducir JavaScript del lado cliente | ||
Añadir interactividad y escuchadores de eventos (onClick() , onChange() , etc) | ||
Usar Estado y Efectos de Ciclo de Vida (useState() , useReducer() , useEffect() ) | ||
Usar APIs exclusivas del navegador | ||
Usar hooks personalizados que dependen de estado, efectos o APIs del navegador | ||
Usar Componentes de Clase React |
Patrones para Componentes de Servidor
Antes de optar por el renderizado del lado cliente, puedes realizar trabajo en el servidor como obtener datos o acceder a tu base de datos o servicios backend.
Aquí hay algunos patrones comunes al trabajar con Componentes de Servidor:
Compartir datos entre componentes
Al obtener datos en el servidor, puede haber casos donde necesites compartir datos entre diferentes componentes. Por ejemplo, puedes tener un layout y una página que dependan de los mismos datos.
En lugar de usar Contexto React (que no está disponible en el servidor) o pasar datos como props, puedes usar fetch
o la función cache
de React para obtener los mismos datos en los componentes que los necesiten, sin preocuparte por hacer solicitudes duplicadas. Esto se debe a que React extiende fetch
para memorizar automáticamente las solicitudes de datos, y la función cache
puede usarse cuando fetch
no está disponible.
Aprende más sobre memorización en React.
Mantener código exclusivo del servidor fuera del entorno cliente
Dado que los módulos JavaScript pueden compartirse entre Componentes de Servidor y Cliente, es posible que código destinado solo al servidor termine en el cliente.
Por ejemplo, considera la siguiente función de obtención de datos:
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
A primera vista, parece que getData
funciona tanto en servidor como cliente. Sin embargo, esta función contiene una API_KEY
, escrita con la intención de que solo se ejecute en el servidor.
Como la variable de entorno API_KEY
no tiene el prefijo NEXT_PUBLIC
, es una variable privada que solo puede accederse en el servidor. Para evitar que tus variables de entorno se filtren al cliente, Next.js reemplaza las variables privadas con una cadena vacía.
Aunque getData()
puede importarse y ejecutarse en el cliente, no funcionará como se espera. Y aunque hacer pública la variable haría que la función funcione en el cliente, puede que no quieras exponer información sensible.
Para prevenir este uso no intencionado de código del servidor en el cliente, podemos usar el paquete server-only
para mostrar un error en tiempo de compilación si alguien importa accidentalmente estos módulos en un Componente Cliente.
Para usar server-only
, primero instala el paquete:
npm install server-only
Luego importa el paquete en cualquier módulo que contenga código exclusivo del servidor:
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
Ahora, cualquier Componente Cliente que importe getData()
recibirá un error en tiempo de compilación indicando que este módulo solo puede usarse en el servidor.
El paquete correspondiente client-only
puede usarse para marcar módulos que contienen código exclusivo del cliente, como código que accede al objeto window
.
Usando paquetes y proveedores de terceros
Dado que los Componentes de Servidor son una nueva característica de React, los paquetes y proveedores de terceros están comenzando a añadir la directiva "use client"
a componentes que usan características exclusivas del cliente como useState
, useEffect
y createContext
.
Hoy, muchos componentes de paquetes npm
que usan características exclusivas del cliente aún no tienen la directiva. Estos componentes de terceros funcionarán como se espera dentro de Componentes Cliente (que ya tienen "use client"
), pero no funcionarán dentro de Componentes de Servidor.
Por ejemplo, supongamos que instalaste el paquete hipotético acme-carousel
que tiene un componente <Carousel />
. Este componente usa useState
, pero aún no tiene la directiva "use client"
.
Si usas <Carousel />
dentro de un Componente Cliente, funcionará como se espera:
'use client'
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
export default function Gallery() {
let [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>Ver imágenes</button>
{/* Funciona, ya que Carousel se usa dentro de un Componente Cliente */}
{isOpen && <Carousel />}
</div>
)
}
'use client'
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
export default function Gallery() {
let [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>Ver imágenes</button>
{/* Funciona, ya que Carousel se usa dentro de un Componente Cliente */}
{isOpen && <Carousel />}
</div>
)
}
Sin embargo, si intentas usarlo directamente dentro de un Componente de Servidor, verás un error:
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>Ver imágenes</p>
{/* Error: `useState` no puede usarse dentro de Componentes de Servidor */}
<Carousel />
</div>
)
}
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>Ver imágenes</p>
{/* Error: `useState` no puede usarse dentro de Componentes de Servidor */}
<Carousel />
</div>
)
}
Esto ocurre porque Next.js no sabe que <Carousel />
usa características exclusivas del cliente.
Para solucionarlo, puedes envolver componentes de terceros que dependan de características del cliente en tus propios Componentes Cliente:
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
Ahora, puedes usar <Carousel />
directamente dentro de un Componente de Servidor:
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>Ver imágenes</p>
{/* Funciona, ya que Carousel es un Componente Cliente */}
<Carousel />
</div>
)
}
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>Ver imágenes</p>
{/* Funciona, ya que Carousel es un Componente Cliente */}
<Carousel />
</div>
)
}
No esperamos que necesites envolver la mayoría de componentes de terceros, ya que es probable que los uses dentro de Componentes Cliente. Sin embargo, una excepción son los proveedores de contexto, ya que dependen del estado y contexto de React, y típicamente se necesitan en la raíz de una aplicación. Aprende más sobre proveedores de contexto de terceros abajo.
Usando Proveedores de Contexto
Los proveedores de contexto típicamente se renderizan cerca de la raíz de una aplicación para compartir preocupaciones globales, como el tema actual. Dado que el contexto React no es compatible con Componentes de Servidor, intentar crear un contexto en la raíz de tu aplicación causará un error:
import { createContext } from 'react'
// createContext no es compatible con Componentes de Servidor
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
import { createContext } from 'react'
// createContext no es compatible con Componentes de Servidor
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
Para solucionarlo, crea tu contexto y renderiza su proveedor dentro de un Componente Cliente:
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({
children,
}: {
children: React.ReactNode
}) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
Ahora tu Componente de Servidor podrá renderizar directamente tu proveedor, ya que está marcado como Componente Cliente:
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
import ThemeProvider from './theme-provider'
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
Con el proveedor renderizado en la raíz, todos los demás Componentes Cliente en tu aplicación podrán consumir este contexto.
Nota importante: Debes renderizar proveedores lo más profundo posible en el árbol - observa cómo
ThemeProvider
solo envuelve{children}
en lugar de todo el documento<html>
. Esto facilita que Next.js optimice las partes estáticas de tus Componentes de Servidor.
Consejos para autores de bibliotecas
De manera similar, los autores de bibliotecas que crean paquetes para ser consumidos por otros desarrolladores pueden usar la directiva "use client"
para marcar puntos de entrada del cliente en su paquete. Esto permite que los usuarios importen componentes del paquete directamente en sus Componentes de Servidor sin necesidad de crear un límite de envoltura.
Puedes optimizar tu paquete usando 'use client' más profundo en el árbol, permitiendo que los módulos importados sean parte del gráfico de módulos del Componente de Servidor.
Vale la pena mencionar que algunos empaquetadores podrían eliminar las directivas "use client"
. Puedes encontrar un ejemplo de cómo configurar esbuild para incluir la directiva "use client"
en los repositorios de React Wrap Balancer y Vercel Analytics.
Componentes Cliente
Mover Componentes Cliente hacia abajo en el árbol
Para reducir el tamaño del paquete JavaScript del cliente, recomendamos mover Componentes Cliente hacia abajo en tu árbol de componentes.
Por ejemplo, puedes tener un Layout con elementos estáticos (logo, enlaces, etc) y una barra de búsqueda interactiva que use estado.
En lugar de hacer todo el layout un Componente Cliente, mueve la lógica interactiva a un Componente Cliente (ej. <SearchBar />
) y mantén tu layout como un Componente de Servidor. Esto significa que no tendrás que enviar todo el JavaScript del layout al cliente.
// SearchBar es un Componente Cliente
import SearchBar from './searchbar'
// Logo es un Componente de Servidor
import Logo from './logo'
// Layout es un Componente de Servidor por defecto
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
// SearchBar es un Componente Cliente
import SearchBar from './searchbar'
// Logo es un Componente de Servidor
import Logo from './logo'
// Layout es un Componente de Servidor por defecto
export default function Layout({ children }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
Pasando props de Servidor a Componentes Cliente (Serialización)
Si obtienes datos en un Componente de Servidor, puedes querer pasarlos como props a Componentes Cliente. Las props pasadas de Servidor a Componentes Cliente deben ser serializables por React.
Si tus Componentes Cliente dependen de datos que no son serializables, puedes obtener datos en el cliente con una biblioteca de terceros o en el servidor mediante un Manejador de Ruta.
Intercalación de Componentes de Servidor y Cliente
Al intercalar Componentes de Cliente y Servidor, puede ser útil visualizar su interfaz de usuario como un árbol de componentes. Comenzando con el layout raíz, que es un Componente de Servidor, luego puede renderizar ciertos subárboles de componentes en el cliente agregando la directiva "use client"
.
Dentro de esos subárboles del cliente, aún puede anidar Componentes de Servidor o llamar a Acciones de Servidor, sin embargo hay algunas cosas a tener en cuenta:
- Durante un ciclo de vida de solicitud-respuesta, su código se mueve del servidor al cliente. Si necesita acceder a datos o recursos en el servidor mientras está en el cliente, hará una nueva solicitud al servidor, no cambiará de un lado a otro.
- Cuando se hace una nueva solicitud al servidor, todos los Componentes de Servidor se renderizan primero, incluidos aquellos anidados dentro de Componentes de Cliente. El resultado renderizado (Carga útil RSC) contendrá referencias a las ubicaciones de los Componentes de Cliente. Luego, en el cliente, React usa la Carga útil RSC para reconciliar los Componentes de Servidor y Cliente en un solo árbol.
- Dado que los Componentes de Cliente se renderizan después de los Componentes de Servidor, no puede importar un Componente de Servidor en un módulo de Componente de Cliente (ya que requeriría una nueva solicitud al servidor). En su lugar, puede pasar un Componente de Servidor como
props
a un Componente de Cliente. Consulte las secciones patrón no admitido y patrón admitido a continuación.
Patrón no admitido: Importar Componentes de Servidor en Componentes de Cliente
El siguiente patrón no está admitido. No puede importar un Componente de Servidor en un Componente de Cliente:
'use client'
// No puede importar un Componente de Servidor en un Componente de Cliente.
import ServerComponent from './Server-Component'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}
'use client'
// No puede importar un Componente de Servidor en un Componente de Cliente.
import ServerComponent from './Server-Component'
export default function ClientComponent({ children }) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ServerComponent />
</>
)
}
Patrón admitido: Pasar Componentes de Servidor a Componentes de Cliente como Props
El siguiente patrón está admitido. Puede pasar Componentes de Servidor como una prop a un Componente de Cliente.
Un patrón común es usar la prop children
de React para crear un "espacio" en su Componente de Cliente.
En el ejemplo siguiente, <ClientComponent>
acepta una prop children
:
'use client'
import { useState } from 'react'
export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
'use client'
import { useState } from 'react'
export default function ClientComponent({ children }) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
<ClientComponent>
no sabe que children
eventualmente se completará con el resultado de un Componente de Servidor. La única responsabilidad que tiene <ClientComponent>
es decidir dónde se colocará eventualmente children
.
En un Componente de Servidor padre, puede importar tanto <ClientComponent>
como <ServerComponent>
y pasar <ServerComponent>
como hijo de <ClientComponent>
:
// Este patrón funciona:
// Puede pasar un Componente de Servidor como hijo o prop de un
// Componente de Cliente.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Las páginas en Next.js son Componentes de Servidor por defecto
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
// Este patrón funciona:
// Puede pasar un Componente de Servidor como hijo o prop de un
// Componente de Cliente.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Las páginas en Next.js son Componentes de Servidor por defecto
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
Con este enfoque, <ClientComponent>
y <ServerComponent>
están desacoplados y pueden renderizarse de forma independiente. En este caso, el hijo <ServerComponent>
puede renderizarse en el servidor, mucho antes de que <ClientComponent>
se renderice en el cliente.
Nota importante:
- El patrón de "elevar el contenido" se ha utilizado para evitar volver a renderizar un componente hijo anidado cuando un componente padre se vuelve a renderizar.
- No está limitado a la prop
children
. Puede usar cualquier prop para pasar JSX.