Que es SWR
SWR (stale-while-revalidate) es una biblioteca de data fetching para React Hooks desarrollada por Vercel. Basada en la estrategia de invalidacion de cache HTTP, permite obtener datos de forma rapida y reactiva.
Principio de funcionamiento de SWR
1. Primera solicitud:
sequenceDiagram
participant Client
participant SWR Cache
participant API Server
Client->>SWR Cache: Solicitud
SWR Cache->>API Server: fetch
API Server-->>SWR Cache: Respuesta
SWR Cache-->>Client: Retorno de datos
2. Re-solicitud (con cache):
sequenceDiagram
participant Client
participant SWR Cache
participant API Server
Client->>SWR Cache: Solicitud
SWR Cache-->>Client: Retorno inmediato de datos antiguos
SWR Cache->>API Server: Revalidacion en segundo plano
API Server-->>SWR Cache: Nuevos datos
SWR Cache-->>Client: Actualizacion con nuevos datos
Uso basico
Instalacion y configuracion
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('La solicitud API ha fallado');
throw error;
}
return res.json();
};
// Fetcher con autenticacion
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) {
// Proceso de refresh del token, etc.
throw new Error('Error de autenticacion');
}
throw new Error('La solicitud API ha fallado');
}
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>Cargando...</div>;
if (error) return <div>Ha ocurrido un error</div>;
if (!data) return null;
return (
<div>
<img src={data.avatar} alt={data.name} />
<h1>{data.name}</h1>
<p>{data.email}</p>
{isValidating && <span>Actualizando...</span>}
<button onClick={() => mutate()}>Recargar</button>
</div>
);
}
Configuracion global
// app/providers.tsx
import { SWRConfig } from 'swr';
import { fetcher } from '@/lib/fetcher';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SWRConfig
value={{
fetcher,
// Configuracion global
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 0,
shouldRetryOnError: true,
errorRetryCount: 3,
errorRetryInterval: 5000,
dedupingInterval: 2000,
// Manejo de errores
onError: (error, key) => {
console.error(`SWR Error [${key}]:`, error);
},
onSuccess: (data, key) => {
console.log(`SWR Success [${key}]:`, data);
},
}}
>
{children}
</SWRConfig>
);
}
Opciones de configuracion SWR
| Categoria | Opcion | Descripcion |
|---|---|---|
| Momento de revalidacion | revalidateOnFocus: true | Al obtener foco |
revalidateOnReconnect: true | Al reconectar | |
refreshInterval: 0 | Actualizacion periodica (ms) | |
refreshWhenHidden: false | Actualizacion cuando esta oculto | |
refreshWhenOffline: false | Actualizacion cuando esta offline | |
| Rendimiento | dedupingInterval: 2000 | Intervalo de deduplicacion (ms) |
focusThrottleInterval: 5000 | Limitacion de foco | |
loadingTimeout: 3000 | Umbral de carga | |
| Manejo de errores | shouldRetryOnError: true | Reintentar en error |
errorRetryCount: 3 | Numero de reintentos | |
errorRetryInterval: 5000 | Intervalo de reintentos |
Fetch condicional
// Solo hacer fetch si el usuario esta logueado
function Dashboard() {
const { user } = useAuth();
// Si user es null, omitir fetch
const { data: profile } = useSWR(
user ? `/api/users/${user.id}/profile` : null,
fetcher
);
// Retornar key con funcion
const { data: posts } = useSWR(
() => (user ? `/api/users/${user.id}/posts` : null),
fetcher
);
return (
<div>
{profile && <ProfileCard profile={profile} />}
{posts && <PostList posts={posts} />}
</div>
);
}
// Fetch con dependencias
function UserPosts({ userId }: { userId: string }) {
const { data: user } = useSWR<User>(`/api/users/${userId}`, fetcher);
// Ejecutar despues de que user sea obtenido
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>
);
}
Mutaciones
Actualizacion optimista
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 };
// Actualizacion optimista: actualizar UI inmediatamente
mutate(
todos?.map(t => t.id === todo.id ? updatedTodo : t),
false // Omitir revalidacion
);
try {
// Enviar a la API
await fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: updatedTodo.completed }),
});
// Revalidar despues del exito
mutate();
} catch (error) {
// Rollback en caso de error
mutate(todos, false);
alert('La actualizacion ha fallado');
}
};
const addTodo = async (title: string) => {
const tempId = `temp-${Date.now()}`;
const newTodo: Todo = { id: tempId, title, completed: false };
// Agregar optimistamente
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();
// Reemplazar ID temporal con 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>
);
}
Mutacion explicita con 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 creado:', result);
} catch (error) {
console.error('Publicacion fallida:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Titulo" required />
<textarea name="content" placeholder="Contenido" required />
<button type="submit" disabled={isMutating}>
{isMutating ? 'Publicando...' : 'Publicar'}
</button>
{error && <p>Error: {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) => {
// Llegamos a la ultima pagina
if (previousPageData && !previousPageData.nextCursor) return null;
// Primera pagina
if (pageIndex === 0) return `/api/posts?limit=${PAGE_SIZE}`;
// Siguiente 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>Cargando...</div>}
{!isReachingEnd && (
<button
onClick={() => setSize(size + 1)}
disabled={isLoadingMore}
>
Cargar mas
</button>
)}
{isReachingEnd && !isEmpty && (
<p>Se han mostrado todas las publicaciones</p>
)}
</div>
);
}
Funcionamiento del 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 → Fin"]
Req0 --> Res0
Res0 --> Req1
Req1 --> Res1
Res1 --> Req2
Req2 --> Res2
Res2 --> End
Hooks personalizados
// 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, // No reintentar en errores de autenticacion
}
);
return {
user: data,
isLoading,
isLoggedIn: !!data && !error,
isError: error,
mutate,
};
}
Prefetch
import { preload } from 'swr';
import { fetcher } from '@/lib/fetcher';
// Prefetch al hacer hover
function PostLink({ postId }: { postId: string }) {
const handleMouseEnter = () => {
preload(`/api/posts/${postId}`, fetcher);
};
return (
<Link
href={`/posts/${postId}`}
onMouseEnter={handleMouseEnter}
>
Ver detalles
</Link>
);
}
// Prefetch al cargar la pagina
function PostsPage() {
useEffect(() => {
// Prefetch posts populares
preload('/api/posts/popular', fetcher);
}, []);
return <div>...</div>;
}
// Datos iniciales con 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>
);
}
Manejo de errores
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(
'La solicitud API ha fallado',
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 no encontrado" />;
case 401:
return <Redirect to="/login" />;
case 500:
return <ErrorPage message="Ha ocurrido un error en el servidor" />;
default:
return <ErrorPage message={error.message} />;
}
}
return <ProfileCard user={data!} />;
}
// Combinacion con Error Boundary
function DataFetchingErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<SWRConfig
value={{
onError: (error, key) => {
// Enviar a servicio de reporte de errores
reportError(error, { key });
},
}}
>
{children}
</SWRConfig>
);
}
Comparacion SWR vs React Query
| Funcionalidad | SWR | React Query |
|---|---|---|
| Tamano del bundle | ~4KB | ~13KB |
| Curva de aprendizaje | Baja | Media |
| DevTools | No | Completo |
| Mutaciones | Simple | Avanzado |
| Scroll infinito | useSWRInfinite | useInfiniteQuery |
| Soporte SSR | Bueno | Bueno |
| Control de cache | Simple | Detallado |