useSWR es el hook de React más potente para data fetching creado por Vercel. Implementa la estrategia Stale-While-Revalidate que revoluciona cómo manejamos datos en aplicaciones React y Next.js.
SWR significa stale-while-revalidate: sirve datos en caché (stale) inmediatamente y revalida en background para traer lo último.
Es una estrategia de caché que significa literalmente: 👉 “Usar lo viejo (stale) mientras traigo lo nuevo (revalidate)”.
🚀 Ventajas principales:
🔑 Conceptos clave:
data (ej: fetch o axios)."use client";
import useSWR from 'swr';
const fetcher = (...args) => fetch(...args).then(r => r.json());
export default function Profile({ id }){
const { data, error, isLoading } = useSWR(id ? `/api/users/${id}` : null, fetcher);
if (error) return <p>Error</p>;
if (isLoading) return <p>Cargando...</p>;
return <div>Hola, {data.name}</div>;
} Notas: usar null como key evita que la petición se ejecute (fetch condicional).
import useSWR from "swr";
const fetcher = (url) => fetch(url).then((res) => res.json());
export default function Users() {
const { data, error, isLoading } = useSWR("/api/users", fetcher);
if (isLoading) return <p>Cargando...</p>;
if (error) return <p>Error al cargar datos</p>;
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
} app/, para usar useSWR el componente debe ser cliente ("use client").// app/profile/[id]/page.jsx (Server component)
import ProfileClient from './ProfileClient';
export default function Page({ params }){
return <ProfileClient id={params.id} />; // ProfileClient contiene useSWR y "use client"
} fallback a SWRConfig para hidratar la caché:// pages/profile/[id].js
import { SWRConfig } from 'swr';
export async function getServerSideProps({ params }){
const res = await fetch(`https://.../users/${params.id}`);
const user = await res.json();
return { props: { fallback: { [`/api/users/${params.id}`]: user } } };
}
export default function Page({ fallback, params }){
return (
<SWRConfig value={{ fallback }}>
<ProfileClient id={params.id} />
</SWRConfig>
);
} Resultado: el cliente ya tiene datos, evita parpadeos.
fetch (básico):const fetcher = (...args) => fetch(...args).then(res => res.json()); axios:import axios from 'axios';
const fetcher = url => axios.get(url).then(r => r.data); graphql-request):import { request } from 'graphql-request';
const fetcher = (query) => request('/graphql', query); SWRConfig)import { SWRConfig } from 'swr';
export default function App({ Component, pageProps }){
return (
<SWRConfig value={{
fetcher: (...args) => fetch(...args).then(r => r.json()),
dedupingInterval: 2000,
errorRetryCount: 3,
}}>
<Component {...pageProps} />
</SWRConfig>
);
} Usa esto para centralizar fetcher y opciones por defecto.
revalidateOnFocus (default true) — revalida al volver al tab.revalidateOnReconnect — revalida al reconectar la red.refreshInterval — polling (ms).dedupingInterval — evita requests duplicados (ms).revalidateIfStale — revalida aunque los datos estén stale.revalidateOnMount — revalidar al montar.Ejemplo con opciones locales:
const { data } = useSWR('/api/data', fetcher, {
refreshInterval: 5000,
revalidateOnFocus: true,
dedupingInterval: 1500,
}); import useSWR, { mutate } from 'swr';
function useTodos(){
const { data: todos } = useSWR('/api/todos');
return { todos };
}
async function addTodoOptimistic(text){
const newTodo = { id: Date.now(), text };
// 1) Actualiza cache local inmediatamente (optimistic)
mutate('/api/todos', current => [...(current||[]), newTodo], false);
// 2) Persiste al servidor
try{
await fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) });
// 3) Revalida y trae datos reales
mutate('/api/todos');
}catch(err){
// revertir: revalidar para recuperar estado real
mutate('/api/todos');
throw err;
}
} Nota: mutate(key, updater, false) actualiza sin revalidar. Luego mutate(key) revalida.
import useSWRInfinite from 'swr/infinite';
const PAGE_SIZE = 10;
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && previousPageData.length === 0) return null; // no más
return `/api/posts?page=${pageIndex + 1}&limit=${PAGE_SIZE}`;
};
export default function Posts(){
const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher);
const posts = (data || []).flat();
return (
<div>
{posts.map(p => <Post key={p.id} {...p} />)}
<button onClick={() => setSize(size + 1)} disabled={isLoading}>Cargar más</button>
</div>
);
} useSWR(shouldFetch ? '/api/x' : null, fetcher)const { data: user } = useSWR(userId ? `/api/user/${userId}` : null, fetcher);
const { data: profile } = useSWR(() => user ? `/api/profile/${user.id}` : null, fetcher); suspense: true si usas React Suspense.// App-level
<SWRConfig value={{ suspense: true }}>
<Suspense fallback={<Spinner/>}>
<Profile id={id} />
</Suspense>
</SWRConfig> Importante: con suspense cambian las formas de manejar loading/error.
mutate:// en hover o en onMouseEnter
mutate('/api/user/123', fetcher('/api/user/123')); import { SWRConfig } from 'swr';
<SWRConfig value={{ provider: () => new Map() }}>
<App />
</SWRConfig> O puedes implementar provider que use localStorage para persistir entre reloads.
null si no quieres fetchear.mutate(key) después si quieres confirmar.dedupingInterval.fetcher — función para obtener datosfallbackData — datos inicialesrevalidateOnFocus — booleanrevalidateOnReconnect — booleanrefreshInterval — ms pollingdedupingInterval — ms dedupesuspense — booleanonErrorRetry — función para controlar reintentosuseSWRInfinite y mutate