SWR con React Server Components
Receta
Obtén datos en un Server Component y pásalos a un Client Component como datos de fallback para SWR. Así obtienes contenido renderizado en el servidor al instante con revalidación en el cliente: lo mejor de ambos mundos.
// app/users/page.tsx (Server Component)
import { UserList } from "./user-list";
async function getUsers() {
const res = await fetch("https://api.example.com/users", {
next: { revalidate: 60 },
});
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return <UserList fallbackData={users} />;
}// app/users/user-list.tsx (Client Component)
"use client";
import useSWR from "swr";
interface User {
id: string;
name: string;
}
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function UserList({ fallbackData }: { fallbackData: User[] }) {
const { data: users } = useSWR<User[]>("/api/users", fetcher, {
fallbackData,
revalidateOnMount: true, // Volver a obtener al montar para datos actualizados
});
return (
<ul>
{users?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Ejemplo funcional
// app/dashboard/page.tsx (Server Component)
import { SWRProvider } from "./swr-provider";
import { DashboardStats } from "./dashboard-stats";
import { RecentOrders } from "./recent-orders";
async function fetchDashboardData() {
const [stats, orders] = await Promise.all([
fetch("https://api.example.com/stats").then((r) => r.json()),
fetch("https://api.example.com/orders?limit=10").then((r) => r.json()),
]);
return { stats, orders };
}
export default async function DashboardPage() {
const { stats, orders } = await fetchDashboardData();
return (
<SWRProvider
fallback={{
"/api/stats": stats,
"/api/orders?limit=10": orders,
}}
>
<h1>Panel</h1>
<DashboardStats />
<RecentOrders />
</SWRProvider>
);
}// app/dashboard/swr-provider.tsx
"use client";
import { SWRConfig } from "swr";
export function SWRProvider({
children,
fallback,
}: {
children: React.ReactNode;
fallback: Record<string, any>;
}) {
return (
<SWRConfig
value={{
fallback,
fetcher: (url: string) => fetch(url).then((r) => r.json()),
}}
>
{children}
</SWRConfig>
);
}// app/dashboard/dashboard-stats.tsx
"use client";
import useSWR from "swr";
interface Stats {
revenue: number;
orders: number;
customers: number;
}
export function DashboardStats() {
// Los datos están disponibles al instante desde el fallback del servidor
const { data: stats } = useSWR<Stats>("/api/stats", {
refreshInterval: 30000, // Mantener las estadísticas actualizadas
});
return (
<div className="grid grid-cols-3 gap-4">
<div>Ingresos: ${stats?.revenue.toLocaleString()}</div>
<div>Pedidos: {stats?.orders}</div>
<div>Clientes: {stats?.customers}</div>
</div>
);
}// app/dashboard/recent-orders.tsx
"use client";
import useSWR from "swr";
interface Order {
id: string;
customer: string;
total: number;
status: string;
}
export function RecentOrders() {
const { data: orders } = useSWR<Order[]>("/api/orders?limit=10");
return (
<table>
<thead>
<tr>
<th>Pedido</th>
<th>Cliente</th>
<th>Total</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
{orders?.map((order) => (
<tr key={order.id}>
<td>{order.id}</td>
<td>{order.customer}</td>
<td>${order.total}</td>
<td>{order.status}</td>
</tr>
))}
</tbody>
</table>
);
}Profundización
Cómo funciona
- Los Server Components obtienen datos en el momento de la solicitud (o en tiempo de compilación con renderizado estático) y los pasan como props o mediante
SWRConfig.fallback. - El
fallbackdeSWRConfiges un mapa de{ [key]: data }que pre-pobla la caché de SWR en el cliente. - Cuando un hook
useSWRse monta con datos de fallback coincidentes, renderiza de inmediato sin estado de carga. - SWR luego revalida en segundo plano (a menos que
revalidateOnMount: false), asegurando que los datos se mantengan actualizados. - Este patrón evita desincronizaciones de hidratación porque el HTML renderizado en el servidor coincide con el renderizado inicial del cliente.
- La API
preloadtambién puede usarse para iniciar la obtención antes de que un componente se monte.
Variaciones
Precarga en Server Component:
// app/products/[id]/page.tsx
import { preload } from "swr";
import { ProductDetail } from "./product-detail";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export default function ProductPage({ params }: { params: { id: string } }) {
// Iniciar la obtención de inmediato
preload(`/api/products/${params.id}`, fetcher);
return <ProductDetail id={params.id} />;
}Fallback por página sin proveedor global:
"use client";
import useSWR from "swr";
export function UserCard({ fallbackData }: { fallbackData: User }) {
const { data } = useSWR("/api/me", fetcher, {
fallbackData, // Fallback por hook
revalidateOnMount: false, // Confiar en los datos del servidor, omitir la revalidación inicial
});
return <div>{data?.name}</div>;
}Streaming con Suspense:
// Server Component
import { Suspense } from "react";
export default function Page() {
return (
<div>
<h1>Panel</h1>
<Suspense fallback={<StatsSkeleton />}>
<StatsLoader />
</Suspense>
</div>
);
}
async function StatsLoader() {
const stats = await fetch("https://api.example.com/stats").then((r) => r.json());
return <StatsClient fallbackData={stats} />;
}Notas de TypeScript
- La prop
fallbackenSWRConfigestá tipada comoRecord<string, any>. No hay comprobación en tiempo de compilación de que las claves coincidan con tus claves deuseSWR. - Para un tipado más estricto, crea un wrapper tipado:
type FallbackMap = {
"/api/stats": Stats;
"/api/orders?limit=10": Order[];
};
function TypedSWRProvider<T extends Record<string, unknown>>({
fallback,
children,
}: {
fallback: T;
children: React.ReactNode;
}) {
return <SWRConfig value={{ fallback }}>{children}</SWRConfig>;
}Errores comunes
- Las claves de
fallbackdeben coincidir exactamente con las claves usadas enuseSWR. Una discrepancia significa que los datos de fallback se ignoran silenciosamente y aparece un estado de carga. - Los datos serializados desde Server Components deben ser compatibles con JSON. Las Dates, Maps, Sets e instancias de clases no sobreviven el límite servidor-cliente.
revalidateOnMount: true(el valor por defecto cuando se proporcionafallbackDatay no se establecerevalidateIfStale) provoca una reobtención inmediata. Establécelo enfalsesi los datos del servidor son lo bastante recientes.- No uses
useSWRdirectamente en Server Components. Los hooks de SWR son solo del cliente. Los Server Components deben usarfetchdirectamente o una capa de datos del lado del servidor. - Cuando usas
fallbackenSWRConfig, los datos están disponibles para todos los hooksuseSWRanidados. Ten cuidado con las colisiones de claves entre componentes no relacionados.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Fallback de SWRConfig | Caché pre-poblada, renderizado instantáneo | Las claves deben coincidir exactamente, sin seguridad de tipos en el fallback |
| fallbackData por hook | Explícito, a nivel de componente | Hay que pasar props desde el servidor |
| Obtención solo en servidor (sin SWR) | Lo más simple, sin JS en el cliente | Sin revalidación en el cliente |
| Hidratación de React Query | dehydrate/hydrate integrados | Biblioteca distinta, más configuración |
FAQs
¿Cómo funciona la opción fallback de SWRConfig con Server Components?
La prop fallback es un mapa { [key]: data } que pre-pobla la caché de SWR en el cliente. Cuando un hook useSWR se monta con una clave coincidente, renderiza de inmediato sin estado de carga y luego revalida en segundo plano.
¿Cuál es la diferencia entre fallbackData por hook y fallback en SWRConfig?
fallbackDatase establece por hookuseSWRcomo prop:useSWR(key, fetcher, { fallbackData }).fallbackenSWRConfigproporciona datos para todos los hooks anidados mediante un mapa clave-valor.- Usa
fallbackDatapara componentes individuales; usafallbackpara pre-población a nivel de página.
Trampa: ¿Qué ocurre si la clave de fallback no coincide exactamente con la clave de useSWR?
Los datos de fallback se ignoran silenciosamente. SWR muestra un estado de carga en lugar de contenido instantáneo. Asegúrate siempre de que las claves en tu mapa fallback coincidan exactamente con las claves usadas en las llamadas a useSWR.
¿Puedo usar useSWR directamente dentro de un Server Component?
No. Los hooks de SWR son solo del cliente. Los Server Components deben usar fetch directamente o una capa de datos del lado del servidor, y luego pasar los datos como props o mediante SWRConfig fallback a Client Components.
¿SWR vuelve a obtener al montar cuando se proporciona fallbackData?
Por defecto, sí (revalidateOnMount: true). Esto asegura que los datos se mantengan actualizados. Establece revalidateOnMount: false si los datos del servidor son lo bastante recientes y quieres omitir la obtención inicial en el cliente.
¿Cómo evito desincronizaciones de hidratación al usar SWR con Server Components?
Pasa los datos obtenidos en el servidor mediante SWRConfig fallback o fallbackData. Esto asegura que el HTML renderizado en el servidor coincida con el renderizado inicial del cliente, evitando errores de hidratación.
Trampa: ¿Qué tipos de datos no pueden sobrevivir el límite servidor-cliente?
Los datos serializados desde Server Components deben ser compatibles con JSON. Las Dates, Maps, Sets e instancias de clases no sobreviven el límite. Conviértelos a objetos planos o cadenas antes de pasarlos.
¿Cómo uso la API preload para iniciar la obtención antes de que un componente se monte?
import { preload } from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export default function Page({ params }: { params: { id: string } }) {
preload(`/api/products/${params.id}`, fetcher);
return <ProductDetail id={params.id} />;
}¿Cómo tipar la prop fallback en SWRConfig con TypeScript?
La prop fallback está tipada como Record<string, any>: no hay comprobación en tiempo de compilación de que las claves coincidan con las de useSWR. Para un tipado más estricto, crea un wrapper tipado:
type FallbackMap = {
"/api/stats": Stats;
"/api/orders?limit=10": Order[];
};
function TypedSWRProvider<T extends Record<string, unknown>>({
fallback,
children,
}: {
fallback: T;
children: React.ReactNode;
}) {
return <SWRConfig value={{ fallback }}>{children}</SWRConfig>;
}¿Cómo combino el streaming de Server Components con SWR?
Usa <Suspense> alrededor de un Server Component asíncrono que obtiene datos y los pasa como fallbackData a un Client Component con useSWR. El servidor transmite el HTML a medida que los datos están disponibles.
¿Qué ocurre con las colisiones de claves al usar fallback entre componentes no relacionados?
Todos los hooks useSWR anidados que comparten la misma clave reciben los mismos datos de fallback. Ten cuidado de que componentes no relacionados no usen accidentalmente la misma clave, lo que haría que compartan datos obsoletos o incorrectos.