TechLead

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