Utilidad de Fetch Centralizada con SWR
Construye una utilidad de fetch única que cada componente cliente referencia — manejo consistente de errores, headers de autenticación, URL base, y almacenamiento en caché en un solo lugar.
Receta
Tarjeta de referencia rápida -- lista para copiar y pegar.
// lib/fetcher.ts
const BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://api.example.com";
export async function fetcher<T>(path: string): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getToken()}`,
},
});
if (!res.ok) {
const error = new Error("Fetch failed") as Error & { status: number };
error.status = res.status;
throw error;
}
return res.json() as Promise<T>;
}
function getToken(): string {
if (typeof window === "undefined") return "";
return localStorage.getItem("token") ?? "";
}// hooks/use-api.ts
"use client";
import useSWR from "swr";
import { fetcher } from "@/lib/fetcher";
export function useApi<T>(path: string | null) {
return useSWR<T>(path, fetcher);
}// Any client component
const { data, error, isLoading } = useApi<Post[]>("/posts");Cuándo usarlo: Tienes múltiples componentes cliente que obtienen datos de la misma API y quieres un lugar para gestionar URL base, autenticación, headers, y forma de error.
Ejemplo Funcional
Paso 1: El Fetcher
El fetcher es una función async simple — sin React, sin hooks. SWR la llama con la clave (la ruta) como primer argumento.
// lib/fetcher.ts
const BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "";
export class ApiError extends Error {
constructor(
message: string,
public status: number,
public data?: unknown
) {
super(message);
this.name = "ApiError";
}
}
export async function fetcher<T>(path: string): Promise<T> {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
// Adjunta token de autenticación si está disponible
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
if (token) headers.Authorization = `Bearer ${token}`;
}
const res = await fetch(`${BASE_URL}${path}`, { headers });
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new ApiError(
body?.message ?? `Request failed: ${res.status}`,
res.status,
body
);
}
return res.json() as Promise<T>;
}Decisiones clave:
- La clase personalizada
ApiErrorllevastatusy respuestadatapara que los consumidores puedan bifurcarse en 401 vs 404 vs 500. - La protección
typeof windowmantiene el fetcher seguro si se llama accidentalmente durante SSR. - URL base desde variable de entorno significa cero cambios de código entre dev/staging/prod.
Paso 2: El Proveedor SWR
Envuelve tu app en SWRConfig para establecer valores por defecto globales — cada llamada useSWR los hereda.
// components/swr-provider.tsx
"use client";
import { SWRConfig } from "swr";
import { fetcher } from "@/lib/fetcher";
export function SWRProvider({ children }: { children: React.ReactNode }) {
return (
<SWRConfig
value={{
fetcher,
revalidateOnFocus: true,
revalidateOnReconnect: true,
errorRetryCount: 3,
dedupingInterval: 2000,
}}
>
{children}
</SWRConfig>
);
}// app/layout.tsx
import { SWRProvider } from "@/components/swr-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SWRProvider>{children}</SWRProvider>
</body>
</html>
);
}¿Por qué un proveedor? Sin él, pasas fetcher a cada llamada useSWR. El proveedor lo establece una vez y cada hook lo hereda.
Paso 3: El Hook
Un envolvente delgado y tipado alrededor de useSWR. Pasa null como clave para omitir la obtención de datos (fetch condicional).
// hooks/use-api.ts
"use client";
import useSWR, { type SWRConfiguration } from "swr";
import type { ApiError } from "@/lib/fetcher";
export function useApi<T>(
path: string | null,
options?: SWRConfiguration<T, ApiError>
) {
const { data, error, isLoading, isValidating, mutate } = useSWR<T, ApiError>(
path,
options
);
return { data, error, isLoading, isValidating, mutate };
}Paso 4: Úsalo en Todas Partes
Cada componente usa el mismo hook. El fetcher maneja autenticación, URL base, y conformación de errores detrás de escenas.
// components/posts-list.tsx
"use client";
import { useApi } from "@/hooks/use-api";
type Post = { id: number; title: string; body: string };
export function PostsList() {
const { data: posts, error, isLoading } = useApi<Post[]>("/posts");
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}// components/user-profile.tsx
"use client";
import { useApi } from "@/hooks/use-api";
type User = { id: number; name: string; email: string };
export function UserProfile({ userId }: { userId: number }) {
const { data: user, isLoading } = useApi<User>(`/users/${userId}`);
if (isLoading) return <p>Loading profile...</p>;
return <h2>{user?.name}</h2>;
}// Fetch condicional -- omite hasta estar listo
function CommentSection({ postId }: { postId: number | null }) {
const { data: comments } = useApi<Comment[]>(
postId ? `/posts/${postId}/comments` : null
);
// ...
}Análisis Profundo
¿Por Qué Centralizar?
Sin un fetcher centralizado, cada componente duplica:
- Construcción de URL base
- Adjunción de header de autenticación
- Análisis de errores y manejo de códigos de estado
- Headers de Content-Type
Un cambio (p. ej., cambiar de tokens localStorage a cookies) significa editar cada llamada fetch. Con la utilidad, cambias un solo archivo.
Deduplicación de SWR
SWR deduplica solicitudes por clave. Si tres componentes en la misma página llaman useApi<User>("/me"), SWR dispara una solicitud de red y comparte el resultado. Por eso la clave importa — es tanto la clave de caché como el argumento del fetcher.
Componente A: useApi("/me") ──┐
Componente B: useApi("/me") ──┼── UN fetch("/me") ── resultado compartido
Componente C: useApi("/me") ──┘
Mutación Después de Escrituras
Después de un POST/PUT/DELETE, dile a SWR que vuelva a obtener datos llamando mutate:
"use client";
import { useApi } from "@/hooks/use-api";
import { fetcher } from "@/lib/fetcher";
import { mutate } from "swr";
export function CreatePostForm() {
async function handleSubmit(formData: FormData) {
await fetch("/api/posts", {
method: "POST",
body: JSON.stringify({ title: formData.get("title") }),
headers: { "Content-Type": "application/json" },
});
// Revalida la lista de posts entre todos los componentes
mutate("/posts");
}
return (
<form action={handleSubmit}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}Actualizaciones Optimistas
SWR soporta actualizaciones optimistas — actualiza el caché inmediatamente, luego revalida en el background:
async function toggleLike(postId: number) {
mutate(
`/posts/${postId}`,
(current: Post | undefined) =>
current ? { ...current, liked: !current.liked } : current,
{ revalidate: true }
);
await fetch(`/api/posts/${postId}/like`, { method: "POST" });
}Polling y Tiempo Real
// Sondea cada 5 segundos
const { data } = useApi<Notification[]>("/notifications", {
refreshInterval: 5000,
});Reintento de Errores con Backoff
SWR reintenta solicitudes fallidas con backoff exponencial de manera predeterminada. Personalízalo globalmente en el proveedor o por hook:
const { data } = useApi<T>("/path", {
errorRetryCount: 5,
onErrorRetry(error, key, config, revalidate, { retryCount }) {
if (error.status === 404) return; // no reintentar 404s
setTimeout(() => revalidate({ retryCount }), 2 ** retryCount * 1000);
},
});Trampas
-
SWR es solo cliente.
useSWRusa estado React internamente. No puedes llamarlo en un Server Component. Para datos del lado del servidor, usafetchdirectamente en el Server Component y pasa los datos como props al componente cliente con SWR para revalidación. -
Las claves deben ser strings estables.
useSWRcompara claves por identidad. Construir claves con literales de plantilla está bien (/posts/${id}), pero no pases objetos como claves — crean nuevas referencias cada renderizado y rompen la deduplicación. -
Clave
nullomite fetch,undefinedno. Para omitir condicionalmente una obtención de datos, pasanullexplícitamente. Pasarundefinedse serializará a"undefined"y disparará una solicitud real. -
El fetcher recibe la clave como su argumento. Si tu clave es
"/posts", el fetcher obtiene"/posts". Si necesitas pasar argumentos extra, usa la forma de clave array:useSWR(["/posts", userId], ([path, id]) => fetcher(path)). -
No desestructures
mutatede la importación global y del retorno del hook. Elmutateglobal deswrrequiere un argumento de clave. Elmutateretornado deuseSWRya está vinculado a la clave de ese hook. Mezclarlos causa bugs silenciosos. -
Los cambios de token de autenticación requieren limpieza de caché. Si el usuario cierra sesión y vuelve a iniciar sesión como un usuario diferente, los datos antiguos en caché de la sesión anterior pueden mostrarse. Llama
mutate(() => true, undefined, { revalidate: true })en cambio de autenticación para limpiar todo el caché de SWR. -
El proveedor debe ser un Client Component.
SWRConfigusa context de React. El componente que lo renderiza necesita"use client". El layout que lo importa puede permanecer como un Server Component.
Alternativas
| Enfoque | Cuándo usarlo |
|---|---|
| TanStack Query | Necesitas mutaciones con actualizaciones optimistas integradas, consultas infinitas, o soporte agnóstico del framework |
Server Components + fetch | Los datos son estáticos o solo se necesitan en tiempo de renderizado — sin caché del lado cliente necesario |
use() + Suspense | Desenvuelve promesas pasadas por servidor en componentes cliente |
Hook useFetch personalizado | Proyectos ligeros que no quieren la dependencia de SWR |
Relacionado
- Obtención de Datos en Server Components — patrones de fetch del lado del servidor
- Almacenamiento en Caché — capas de almacenamiento en caché de Next.js
- Revalidación — revalidación bajo demanda y basada en tiempo
- Streaming — carga progresiva de datos con Suspense