O que e SWR
SWR (stale-while-revalidate) e uma biblioteca de data fetching para React Hooks desenvolvida pela Vercel. Baseada na estrategia de invalidacao de cache HTTP, ela proporciona busca de dados rapida e reativa.
Principio de Funcionamento do SWR
1. Primeira requisicao:
sequenceDiagram
participant Client
participant SWR Cache
participant API Server
Client->>SWR Cache: Requisicao
SWR Cache->>API Server: fetch
API Server-->>SWR Cache: Resposta
SWR Cache-->>Client: Retornar dados
2. Nova requisicao (com cache):
sequenceDiagram
participant Client
participant SWR Cache
participant API Server
Client->>SWR Cache: Requisicao
SWR Cache-->>Client: Retornar dados antigos imediatamente
SWR Cache->>API Server: Revalidar em background
API Server-->>SWR Cache: Novos dados
SWR Cache-->>Client: Atualizar para novos dados
Uso Basico
Instalacao e Configuracao
npm install swr
// lib/fetcher.ts
export const fetcher = async <T>(url: string): Promise<T> => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error('Falha na requisicao da API');
throw error;
}
return res.json();
};
// Fetcher com autenticacao
export const authFetcher = async <T>(url: string): Promise<T> => {
const token = localStorage.getItem('token');
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) {
if (res.status === 401) {
// Processar refresh de token, etc.
throw new Error('Erro de autenticacao');
}
throw new Error('Falha na requisicao da API');
}
return res.json();
};
Hook useSWR
import useSWR from 'swr';
import { fetcher } from '@/lib/fetcher';
interface User {
id: string;
name: string;
email: string;
avatar: string;
}
function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading, isValidating, mutate } = useSWR<User>(
`/api/users/${userId}`,
fetcher
);
if (isLoading) return <div>Carregando...</div>;
if (error) return <div>Ocorreu um erro</div>;
if (!data) return null;
return (
<div>
<img src={data.avatar} alt={data.name} />
<h1>{data.name}</h1>
<p>{data.email}</p>
{isValidating && <span>Atualizando...</span>}
<button onClick={() => mutate()}>Recarregar</button>
</div>
);
}
Configuracao Global
// app/providers.tsx
import { SWRConfig } from 'swr';
import { fetcher } from '@/lib/fetcher';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SWRConfig
value={{
fetcher,
// Configuracao global
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 0,
shouldRetryOnError: true,
errorRetryCount: 3,
errorRetryInterval: 5000,
dedupingInterval: 2000,
// Tratamento de erros
onError: (error, key) => {
console.error(`SWR Error [${key}]:`, error);
},
onSuccess: (data, key) => {
console.log(`SWR Success [${key}]:`, data);
},
}}
>
{children}
</SWRConfig>
);
}
Opcoes de Configuracao do SWR
| Categoria | Opcao | Descricao |
|---|---|---|
| Timing de Revalidacao | revalidateOnFocus: true | Ao focar |
revalidateOnReconnect: true | Ao reconectar | |
refreshInterval: 0 | Atualizacao periodica (ms) | |
refreshWhenHidden: false | Atualizar quando oculto | |
refreshWhenOffline: false | Atualizar quando offline | |
| Performance | dedupingInterval: 2000 | Intervalo de deduplicacao (ms) |
focusThrottleInterval: 5000 | Limite de foco | |
loadingTimeout: 3000 | Limite de loading | |
| Tratamento de Erros | shouldRetryOnError: true | Retry em caso de erro |
errorRetryCount: 3 | Numero de retries | |
errorRetryInterval: 5000 | Intervalo de retry |
Fetch Condicional
// Fetch apenas se o usuario estiver logado
function Dashboard() {
const { user } = useAuth();
// Se user for null, pula o fetch
const { data: profile } = useSWR(
user ? `/api/users/${user.id}/profile` : null,
fetcher
);
// Retornar key com funcao
const { data: posts } = useSWR(
() => (user ? `/api/users/${user.id}/posts` : null),
fetcher
);
return (
<div>
{profile && <ProfileCard profile={profile} />}
{posts && <PostList posts={posts} />}
</div>
);
}
// Fetch com dependencia
function UserPosts({ userId }: { userId: string }) {
const { data: user } = useSWR<User>(`/api/users/${userId}`, fetcher);
// Executar apos user ser carregado
const { data: posts } = useSWR<Post[]>(
user ? `/api/users/${user.id}/posts` : null,
fetcher
);
return (
<div>
<h1>Posts de {user?.name}</h1>
{posts?.map(post => <PostCard key={post.id} post={post} />)}
</div>
);
}
Mutacao
Atualizacao Otimista
import useSWR, { useSWRConfig } from 'swr';
interface Todo {
id: string;
title: string;
completed: boolean;
}
function TodoList() {
const { data: todos, mutate } = useSWR<Todo[]>('/api/todos', fetcher);
const { mutate: globalMutate } = useSWRConfig();
const toggleTodo = async (todo: Todo) => {
const updatedTodo = { ...todo, completed: !todo.completed };
// Atualizacao otimista: atualizar UI imediatamente
mutate(
todos?.map(t => t.id === todo.id ? updatedTodo : t),
false // Pular revalidacao
);
try {
// Enviar para API
await fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: updatedTodo.completed }),
});
// Revalidar apos sucesso
mutate();
} catch (error) {
// Rollback em caso de erro
mutate(todos, false);
alert('Falha na atualizacao');
}
};
const addTodo = async (title: string) => {
const tempId = `temp-${Date.now()}`;
const newTodo: Todo = { id: tempId, title, completed: false };
// Adicionar otimisticamente
mutate([...(todos || []), newTodo], false);
try {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
const createdTodo = await res.json();
// Substituir ID temporario pelo ID real
mutate(
todos?.map(t => t.id === tempId ? createdTodo : t),
false
);
} catch (error) {
// Rollback
mutate(todos?.filter(t => t.id !== tempId), false);
}
};
return (
<ul>
{todos?.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo)}>
<input type="checkbox" checked={todo.completed} readOnly />
{todo.title}
</li>
))}
</ul>
);
}
Mutacao Explicita com useSWRMutation
import useSWRMutation from 'swr/mutation';
interface CreatePostInput {
title: string;
content: string;
}
async function createPost(url: string, { arg }: { arg: CreatePostInput }) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(arg),
});
return res.json();
}
function CreatePostForm() {
const { trigger, isMutating, error } = useSWRMutation(
'/api/posts',
createPost
);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
try {
const result = await trigger({
title: formData.get('title') as string,
content: formData.get('content') as string,
});
console.log('Post criado:', result);
} catch (error) {
console.error('Falha ao criar post:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Titulo" required />
<textarea name="content" placeholder="Conteudo" required />
<button type="submit" disabled={isMutating}>
{isMutating ? 'Publicando...' : 'Publicar'}
</button>
{error && <p>Erro: {error.message}</p>}
</form>
);
}
Scroll Infinito
import useSWRInfinite from 'swr/infinite';
interface Post {
id: string;
title: string;
content: string;
}
interface PostsResponse {
posts: Post[];
nextCursor: string | null;
}
const PAGE_SIZE = 10;
function InfinitePostList() {
const getKey = (pageIndex: number, previousPageData: PostsResponse | null) => {
// Chegou na ultima pagina
if (previousPageData && !previousPageData.nextCursor) return null;
// Primeira pagina
if (pageIndex === 0) return `/api/posts?limit=${PAGE_SIZE}`;
// Proxima pagina
return `/api/posts?cursor=${previousPageData?.nextCursor}&limit=${PAGE_SIZE}`;
};
const {
data,
error,
size,
setSize,
isLoading,
isValidating,
} = useSWRInfinite<PostsResponse>(getKey, fetcher);
const posts = data?.flatMap(page => page.posts) ?? [];
const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');
const isEmpty = data?.[0]?.posts.length === 0;
const isReachingEnd = isEmpty || (data && !data[data.length - 1]?.nextCursor);
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
{isLoadingMore && <div>Carregando...</div>}
{!isReachingEnd && (
<button
onClick={() => setSize(size + 1)}
disabled={isLoadingMore}
>
Carregar mais
</button>
)}
{isReachingEnd && !isEmpty && (
<p>Todos os posts foram exibidos</p>
)}
</div>
);
}
Funcionamento do Scroll Infinito
flowchart TB
subgraph Page0["pageIndex: 0"]
Req0["/api/posts?limit=10"]
Res0["{ posts: [...], nextCursor: 'abc' }"]
end
subgraph Page1["pageIndex: 1"]
Req1["/api/posts?cursor=abc&limit=10"]
Res1["{ posts: [...], nextCursor: 'def' }"]
end
subgraph Page2["pageIndex: 2"]
Req2["/api/posts?cursor=def&limit=10"]
Res2["{ posts: [...], nextCursor: null }"]
end
End["nextCursor: null → Fim"]
Req0 --> Res0
Res0 --> Req1
Req1 --> Res1
Res1 --> Req2
Req2 --> Res2
Res2 --> End
Hooks Customizados
// hooks/usePosts.ts
import useSWR from 'swr';
import { fetcher } from '@/lib/fetcher';
interface Post {
id: string;
title: string;
content: string;
author: { id: string; name: string };
createdAt: string;
}
interface UsePostsOptions {
limit?: number;
tag?: string;
}
export function usePosts(options: UsePostsOptions = {}) {
const { limit = 10, tag } = options;
const params = new URLSearchParams();
params.set('limit', String(limit));
if (tag) params.set('tag', tag);
return useSWR<Post[]>(
`/api/posts?${params.toString()}`,
fetcher,
{
revalidateOnFocus: false,
dedupingInterval: 10000,
}
);
}
export function usePost(id: string | null) {
return useSWR<Post>(
id ? `/api/posts/${id}` : null,
fetcher
);
}
// hooks/useUser.ts
export function useUser() {
const { data, error, isLoading, mutate } = useSWR<User>(
'/api/auth/me',
fetcher,
{
revalidateOnFocus: true,
errorRetryCount: 0, // Nao fazer retry em erros de autenticacao
}
);
return {
user: data,
isLoading,
isLoggedIn: !!data && !error,
isError: error,
mutate,
};
}
Prefetch
import { preload } from 'swr';
import { fetcher } from '@/lib/fetcher';
// Prefetch no hover
function PostLink({ postId }: { postId: string }) {
const handleMouseEnter = () => {
preload(`/api/posts/${postId}`, fetcher);
};
return (
<Link
href={`/posts/${postId}`}
onMouseEnter={handleMouseEnter}
>
Ver detalhes
</Link>
);
}
// Prefetch ao carregar pagina
function PostsPage() {
useEffect(() => {
// Prefetch de posts populares
preload('/api/posts/popular', fetcher);
}, []);
return <div>...</div>;
}
// Dados iniciais com SSR/SSG
export async function getStaticProps() {
const posts = await fetcher('/api/posts');
return {
props: {
fallback: {
'/api/posts': posts,
},
},
};
}
function Page({ fallback }) {
return (
<SWRConfig value={{ fallback }}>
<PostList />
</SWRConfig>
);
}
Tratamento de Erros
import useSWR from 'swr';
class APIError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new APIError(
'Falha na requisicao da API',
res.status
);
throw error;
}
return res.json();
};
function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading } = useSWR<User, APIError>(
`/api/users/${userId}`,
fetcher
);
if (isLoading) return <LoadingSpinner />;
if (error) {
switch (error.status) {
case 404:
return <NotFound message="Usuario nao encontrado" />;
case 401:
return <Redirect to="/login" />;
case 500:
return <ErrorPage message="Erro do servidor" />;
default:
return <ErrorPage message={error.message} />;
}
}
return <ProfileCard user={data!} />;
}
// Combinacao com Error Boundary
function DataFetchingErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<SWRConfig
value={{
onError: (error, key) => {
// Enviar para servico de relatorio de erros
reportError(error, { key });
},
}}
>
{children}
</SWRConfig>
);
}
Comparacao SWR vs React Query
| Recurso | SWR | React Query |
|---|---|---|
| Tamanho do bundle | ~4KB | ~13KB |
| Curva de aprendizado | Baixa | Media |
| DevTools | Nao | Rico |
| Mutacao | Simples | Avancada |
| Scroll infinito | useSWRInfinite | useInfiniteQuery |
| Suporte SSR | Bom | Bom |
| Controle de cache | Simples | Detalhado |