SWR
React hooks para obtención de datos con estrategia stale-while-revalidate por Vercel
SWR - Stale-While-Revalidate
SWR es una biblioteca de React hooks para obtención de datos creada por Vercel. El nombre proviene de la estrategia de invalidación de caché HTTP "stale-while-revalidate" - devuelve datos cacheados (obsoletos) primero, luego obtiene datos frescos en segundo plano. Es ligera, rápida y se integra perfectamente con Next.js.
Características Clave
- Ligera — Solo ~4KB gzipped
- Rápida — UI instantánea con datos cacheados
- Revalidación — Actualizaciones automáticas en segundo plano
- Revalidación al Enfocar — Revalida cuando la pestaña recupera el foco
- Integración Next.js — Funciona genial con Next.js
Instalación
npm install swr
Uso Básico
import useSWR from 'swr';
// Función fetcher
const fetcher = (url) => fetch(url).then(res => res.json());
function Profile() {
const { data, error, isLoading, mutate } = useSWR('/api/user', fetcher);
if (isLoading) return <div>Cargando...</div>;
if (error) return <div>Error al cargar</div>;
return (
<div>
<h1>Hola, {data.name}!</h1>
<button onClick={() => mutate()}>Actualizar</button>
</div>
);
}
Configuración Global
import { SWRConfig } from 'swr';
// Fetcher y opciones globales
function App() {
return (
<SWRConfig
value={{
fetcher: (url) => fetch(url).then(res => res.json()),
refreshInterval: 3000, // Actualiza cada 3s
revalidateOnFocus: true, // Revalida al enfocar ventana
dedupingInterval: 2000, // Deduplica solicitudes dentro de 2s
onError: (error) => {
console.error('Error SWR:', error);
},
}}
>
<MyApp />
</SWRConfig>
);
}
// Ahora los componentes no necesitan especificar fetcher
function UserProfile() {
const { data } = useSWR('/api/user');
return <div>{data?.name}</div>;
}
Claves Dinámicas
import useSWR from 'swr';
function UserPosts({ userId }) {
// La clave puede ser string, array o null
const { data: posts } = useSWR(
userId ? `/api/users/${userId}/posts` : null,
fetcher
);
// Claves de array para fetchers complejos
const { data } = useSWR(
['/api/posts', userId, 'published'],
([url, id, status]) => fetchPosts(url, id, status)
);
// Obtención condicional
const { data: user } = useSWR('/api/user', fetcher);
const { data: projects } = useSWR(
// Solo obtiene cuando el usuario está cargado
user ? `/api/users/${user.id}/projects` : null,
fetcher
);
return (/* ... */);
}
Mutación y Revalidación
import useSWR, { useSWRConfig } from 'swr';
function TodoList() {
const { data: todos, mutate } = useSWR('/api/todos', fetcher);
const addTodo = async (text) => {
// Actualización optimista
const optimisticTodos = [...todos, { id: Date.now(), text, completed: false }];
// Actualiza datos locales inmediatamente (optimista)
// Establece revalidate a false para prevenir revalidación inmediata
mutate(optimisticTodos, false);
// Envía solicitud al servidor
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
});
// Revalida para asegurar que los datos sean correctos
mutate();
};
const toggleTodo = async (id) => {
// Actualización optimista
mutate(
todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t),
false
);
await fetch(`/api/todos/${id}/toggle`, { method: 'PATCH' });
mutate();
};
return (/* ... */);
}
// Mutación global
function LogoutButton() {
const { mutate } = useSWRConfig();
const logout = async () => {
await fetch('/api/logout', { method: 'POST' });
// Invalida todas las claves que coincidan con el patrón
mutate(key => typeof key === 'string' && key.startsWith('/api/user'));
// O limpiar todo el caché
mutate(() => true, undefined, { revalidate: false });
};
return <button onClick={logout}>Cerrar Sesión</button>;
}
Paginación
import useSWR from 'swr';
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, isLoading } = useSWR(
`/api/posts?page=${page}&limit=10`,
fetcher,
{ keepPreviousData: true } // Mantiene mostrando datos anteriores mientras carga nuevos
);
return (
<div>
{data?.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
<button
disabled={page === 1}
onClick={() => setPage(p => p - 1)}
>
Anterior
</button>
<span>Página {page}</span>
<button
disabled={!data?.hasMore}
onClick={() => setPage(p => p + 1)}
>
Siguiente
</button>
</div>
);
}
Carga Infinita
import useSWRInfinite from 'swr/infinite';
function InfinitePostList() {
const getKey = (pageIndex, previousPageData) => {
// Alcanzó el final
if (previousPageData && !previousPageData.hasMore) return null;
// Primera página
if (pageIndex === 0) return '/api/posts?page=1';
// Páginas siguientes
return `/api/posts?page=${pageIndex + 1}`;
};
const { data, size, setSize, isLoading, isValidating } = useSWRInfinite(
getKey,
fetcher
);
const posts = data ? 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]?.hasMore);
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
<button
disabled={isLoadingMore || isReachingEnd}
onClick={() => setSize(size + 1)}
>
{isLoadingMore ? 'Cargando...' : isReachingEnd ? 'No Hay Más' : 'Cargar Más'}
</button>
</div>
);
}
SWR vs TanStack Query
| Característica | SWR | TanStack Query |
|---|---|---|
| Tamaño del bundle | ~4KB | ~12KB |
| Complejidad de API | Más simple | Más opciones |
| Mutaciones | Manual con mutate() | Hook useMutation |
| DevTools | Comunidad | Oficial |
| Mejor para | Necesidades simples, Next.js | Requisitos de datos complejos |
💡 Mejores Prácticas
- • Usa SWRConfig para fetcher y opciones globales
- • Devuelve null como clave para deshabilitar condicionalmente la obtención
- • Usa mutate() para actualizaciones optimistas
- • Establece intervalos de revalidación apropiados para tus datos
- • Usa keepPreviousData para paginación más suave
- • Considera TanStack Query para necesidades de mutación complejas