Utilidade de Fetch Centralizada com SWR
Construa uma única utilidade de fetch que todos os componentes cliente referenciam - tratamento de erros consistente, cabeçalhos de autenticação, URL base e cache em um só lugar.
Busque em todas as páginas da documentação
Construa uma única utilidade de fetch que todos os componentes cliente referenciam - tratamento de erros consistente, cabeçalhos de autenticação, URL base e cache em um só lugar.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
// 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");Quando usar isso: Você tem vários componentes cliente que buscam dados da mesma API e deseja um local para gerenciar a URL base, autenticação, cabeçalhos e formato de erro.
O fetcher é uma função async simples - sem React, sem hooks. SWR o chama com a chave (o caminho) como primeiro 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",
};
// Attach auth token if available
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>;
}Decisões chave:
ApiError personalizada carrega o status e os data da resposta para que os consumidores possam ramificar entre 401, 404 ou 500.typeof window mantém o fetcher seguro se acidentalmente chamado durante o SSR.Envolva seu aplicativo com SWRConfig para definir padrões globais - cada chamada useSWR os herda.
// 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 que um provedor? Sem ele, você passa fetcher para cada useSWR. O provedor o define uma vez e cada hook o herda.
Um wrapper fino e tipado em torno de useSWR. Passe null como chave para pular o fetch (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 o mesmo hook. O fetcher lida com autenticação, URL base e formatação de erros nos bastidores.
// 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>;
}// Conditional fetching -- skip until ready
function CommentSection({ postId }: { postId: number | null }) {
const { data: comments } = useApi<Comment[]>(
postId ? `/posts/${postId}/comments` : null
);
// ...
}Sem um fetcher centralizado, cada componente duplica:
Uma alteração (por exemplo, mudar de tokens localStorage para cookies) significa editar cada chamada de fetch. Com a utilidade, você altera um arquivo.
SWR desduplica requisições por chave. Se três componentes na mesma página chamarem useApi<User>("/me"), SWR dispara uma requisição de rede e compartilha o resultado. É por isso que a chave importa - é tanto a chave de cache quanto o argumento do fetcher.
Componente A: useApi("/me") ──┐
Componente B: useApi("/me") ──┼── UM fetch("/me") ── resultado compartilhado
Componente C: useApi("/me") ──┘
Após um POST/PUT/DELETE, informe ao SWR para buscar novamente chamando 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" },
});
// Revalidate the posts list across all components
mutate("/posts");
}
return (
<form action={handleSubmit}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}SWR suporta atualizações otimistas - atualiza o cache imediatamente e, em seguida, revalida em segundo plano:
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" });
}// Poll every 5 seconds
const { data } = useApi<Notification[]>("/notifications", {
refreshInterval: 5000,
});SWR tenta novamente requisições falhas com backoff exponencial por padrão. Personalize globalmente no provedor ou por hook:
const { data } = useApi<T>("/path", {
errorRetryCount: 5,
onErrorRetry(error, key, config, revalidate, { retryCount }) {
if (error.status === 404) return; // don't retry 404s
setTimeout(() => revalidate({ retryCount }), 2 ** retryCount * 1000);
},
});SWR é apenas para cliente. useSWR usa estado do React internamente. Você não pode chamá-lo em um Server Component. Para dados do lado do servidor, use fetch diretamente no Server Component e passe os dados como props para o componente cliente com SWR para revalidação.
Chaves devem ser strings estáveis. useSWR compara chaves por identidade. Construir chaves com literais de template é bom (/posts/${id}), mas não passe objetos como chaves - eles criam novas referências a cada renderização e quebram a desduplicação.
Chave null pula o fetch, undefined não. Para pular condicionalmente um fetch, passe null explicitamente. Passar undefined será serializado para "undefined" e disparará uma requisição real.
O fetcher recebe a chave como seu argumento. Se sua chave for "/posts", o fetcher recebe "/posts". Se você precisar passar argumentos extras, use a forma de chave de array: useSWR(["/posts", userId], ([path, id]) => fetcher(path)).
Não desestruture mutate da importação global e do retorno do hook. O mutate global de swr requer um argumento de chave. O mutate retornado de useSWR já está vinculado à chave desse hook. Misturá-los causa bugs silenciosos.
Mudanças no token de autenticação exigem limpeza do cache. Se o usuário sair e entrar novamente como um usuário diferente, dados em cache desatualizados da sessão anterior podem ser exibidos. Chame mutate(() => true, undefined, { revalidate: true }) na mudança de autenticação para limpar todo o cache SWR.
O provedor deve ser um Client Component. SWRConfig usa contexto React. O componente que o renderiza precisa de "use client". O layout que o importa pode permanecer um Server Component.
| Abordagem | Quando usar |
|---|---|
| TanStack Query | Precisa de mutações com atualizações otimistas integradas, consultas infinitas ou suporte agnóstico de framework |
Server Components + fetch | Os dados são estáticos ou só precisam no tempo de renderização - nenhum cache do lado do cliente é necessário |
use() + Suspense | Desembrulhando promessas passadas pelo servidor em componentes cliente |
Hook useFetch personalizado | Projetos leves que não querem a dependência do SWR |
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥