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.
Busca en todas las páginas de la documentación
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.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
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:
ApiError lleva status y respuesta data para que los consumidores puedan bifurcarse en 401 vs 404 vs 500.typeof window mantiene el fetcher seguro si se llama accidentalmente durante SSR.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.
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 };
}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
);
// ...
}Sin un fetcher centralizado, cada componente duplica:
Un cambio (p. ej., cambiar de tokens localStorage a cookies) significa editar cada llamada fetch. Con la utilidad, cambias un solo archivo.
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") ──┘
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>
);
}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" });
}// Sondea cada 5 segundos
const { data } = useApi<Notification[]>("/notifications", {
refreshInterval: 5000,
});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);
},
});SWR es solo cliente. useSWR usa estado React internamente. No puedes llamarlo en un Server Component. Para datos del lado del servidor, usa fetch directamente en el Server Component y pasa los datos como props al componente cliente con SWR para revalidación.
Las claves deben ser strings estables. useSWR compara 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 null omite fetch, undefined no. Para omitir condicionalmente una obtención de datos, pasa null explícitamente. Pasar undefined se 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 mutate de la importación global y del retorno del hook. El mutate global de swr requiere un argumento de clave. El mutate retornado de useSWR ya 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. SWRConfig usa context de React. El componente que lo renderiza necesita "use client". El layout que lo importa puede permanecer como un Server Component.
| 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 |
Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥