Como Usar SWR em React: Estratégia Stale-While-Revalidate na Prática em Produção Já leu

SWR em React: Estratégia Stale-While-Revalidate na Prática O que é SWR e por que importa SWR (Stale-While-Revalidate) é uma estratégia de cache HTTP que permite servir dados em cache (mesmo que desatualizados) imediatamente ao usuário, enquanto faz uma requisição em segundo plano para atualizar esses dados. Em React, a biblioteca SWR implementa esse padrão de forma elegante e produtiva, reduzindo a latência percebida e melhorando a experiência do usuário. A ideia central é simples: não force o usuário a esperar por dados frescos. Mostre o que você tem agora e atualize em background. Isso é particularmente poderoso em aplicações que lidam com conexões lentas ou instáveis, onde a percepção de performance importa tanto quanto a performance real. A biblioteca SWR, mantida pela Vercel, abstrai toda a complexidade dessa estratégia, oferecendo um hook simples e intuitivo. Entendendo o Ciclo de Vida do SWR Como funciona internamente Quando você usa SWR pela primeira vez com uma chave específica, ele segue este fluxo:

SWR em React: Estratégia Stale-While-Revalidate na Prática

O que é SWR e por que importa

SWR (Stale-While-Revalidate) é uma estratégia de cache HTTP que permite servir dados em cache (mesmo que desatualizados) imediatamente ao usuário, enquanto faz uma requisição em segundo plano para atualizar esses dados. Em React, a biblioteca SWR implementa esse padrão de forma elegante e produtiva, reduzindo a latência percebida e melhorando a experiência do usuário.

A ideia central é simples: não force o usuário a esperar por dados frescos. Mostre o que você tem agora e atualize em background. Isso é particularmente poderoso em aplicações que lidam com conexões lentas ou instáveis, onde a percepção de performance importa tanto quanto a performance real. A biblioteca SWR, mantida pela Vercel, abstrai toda a complexidade dessa estratégia, oferecendo um hook simples e intuitivo.

Entendendo o Ciclo de Vida do SWR

Como funciona internamente

Quando você usa SWR pela primeira vez com uma chave específica, ele segue este fluxo: primeiro, verifica se existe dados em cache; se existirem, retorna imediatamente (estado "stale"). Em paralelo, faz uma requisição para o servidor. Quando a resposta chega, atualiza o cache e o componente re-renderiza com os dados frescos. Se nenhum cache existir, ele aguarda a primeira resposta antes de renderizar.

Este comportamento elimina aquele padrão comum onde você mostra um skeleton loader ou message de "carregando". Com SWR, você pode mostrar dados anteriores enquanto revalida, criando uma experiência fluida. Entender esse ciclo é crucial para aproveitar o máximo potencial da biblioteca.

Exemplo básico funcional

import useSWR from 'swr';

// Função fetcher que será usada para fazer requisições
const fetcher = (url) => fetch(url).then(res => res.json());

export function UserProfile({ userId }) {
  // Hook básico do SWR
  const { data, error, isLoading } = useSWR(
    `/api/users/${userId}`,
    fetcher
  );

  if (isLoading) return <div>Carregando...</div>;
  if (error) return <div>Erro ao carregar: {error.message}</div>;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>Email: {data.email}</p>
    </div>
  );
}

Note que isLoading é true apenas na primeira requisição. Nas subsequentes, dados anteriores são mostrados imediatamente enquanto a revalidação acontece em background. Isso é o SWR em ação.

Configuração Avançada e Otimizações

Opções essenciais

SWR oferece dúzias de opções, mas algumas são críticas para produção. A opção revalidateOnFocus controla se os dados devem ser revalidados quando a aba volta ao foco — ideal para manter dados sempre frescos. dedupingInterval define quanto tempo (em ms) requisições duplicadas são deduplificadas, economizando banda. focusThrottleInterval evita múltiplas revalidações rápidas.

Outra opção importante é errorRetryCount e errorRetryInterval, que definem quantas vezes tentar novamente em caso de erro e com qual intervalo. Em produção, você provavelmente quer um revalidateIfStale: true para sempre revalidar se os dados estão muito antigos.

import useSWR from 'swr';
import axios from 'axios';

const fetcher = (url) => axios.get(url).then(res => res.data);

export function ProductList() {
  const { data: products, mutate } = useSWR(
    '/api/products',
    fetcher,
    {
      revalidateOnFocus: true,
      revalidateOnReconnect: true,
      dedupingInterval: 60000, // 1 minuto
      focusThrottleInterval: 300000, // 5 minutos
      errorRetryCount: 3,
      errorRetryInterval: 5000,
      shouldRetryOnError: true,
    }
  );

  const handleRefresh = () => {
    // Force uma revalidação imediata
    mutate();
  };

  if (!products) return <div>Nenhum produto carregado ainda</div>;

  return (
    <div>
      <button onClick={handleRefresh}>Atualizar</button>
      {products.map(p => (
        <div key={p.id}>{p.name}</div>
      ))}
    </div>
  );
}

Mutação otimista (Optimistic UI)

Um dos padrões mais poderosos é a mutação otimista: atualizar a UI imediatamente enquanto a requisição está em flight. Se falhar, você reverte. Isso cria uma sensação de resposta instantânea.

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(res => res.json());

export function TodoItem({ todoId, initialData }) {
  const { data: todo, mutate } = useSWR(
    `/api/todos/${todoId}`,
    fetcher,
    {
      fallbackData: initialData,
    }
  );

  const handleToggleTodo = async () => {
    // Estado otimista: atualiza localmente
    const newData = { ...todo, completed: !todo.completed };
    mutate(newData, false); // false = não revalidar

    try {
      const response = await fetch(`/api/todos/${todoId}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ completed: newData.completed }),
      });

      if (!response.ok) throw new Error('Falha ao atualizar');

      // Revalida para garantir sincronismo com servidor
      mutate();
    } catch (error) {
      // Reverte em caso de erro
      mutate(todo, false);
      alert('Erro ao atualizar todo');
    }
  };

  return (
    <div>
      <input
        type="checkbox"
        checked={todo?.completed}
        onChange={handleToggleTodo}
      />
      <span>{todo?.title}</span>
    </div>
  );
}

Aqui, mutate(newData, false) atualiza a UI imediatamente sem fazer fetch. Depois tentamos sincronizar com o servidor. Se falhar, revertemos com mutate(todo, false).

Padrões de Produção

Múltiplas requisições correlacionadas

Frequentemente você precisa fazer várias requisições que dependem uma da outra. SWR oferece o padrão de "dependent requests" para isso.

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(res => res.json());

export function UserWithPosts({ userId }) {
  // Primeira requisição: dados do usuário
  const { data: user } = useSWR(
    userId ? `/api/users/${userId}` : null,
    fetcher
  );

  // Segunda requisição: posts do usuário (depende da primeira)
  const { data: posts } = useSWR(
    user ? `/api/users/${user.id}/posts` : null,
    fetcher
  );

  if (!user) return <div>Carregando usuário...</div>;
  if (!posts) return <div>Carregando posts...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

Passando null como chave, o SWR não faz a requisição. Isso permite criar dependências naturalmente.

Tratamento robusto de erros

import useSWR from 'swr';

const fetcher = async (url) => {
  const res = await fetch(url);
  if (!res.ok) {
    const error = new Error('API Error');
    error.status = res.status;
    error.info = await res.json();
    throw error;
  }
  return res.json();
};

export function Dashboard() {
  const { data, error, isLoading } = useSWR(
    '/api/dashboard',
    fetcher,
    {
      onError: (err) => {
        console.error(`Erro ${err.status}:`, err.info);
      },
      onErrorRetry: (error, key, config, retryer, { retryCount }) => {
        if (error.status === 404) return; // Não tenta novamente
        if (retryCount >= 3) return; // Máximo de tentativas
        setTimeout(() => retryer({ retryCount }), 1000);
      },
    }
  );

  if (isLoading) return <div>Carregando dashboard...</div>;
  if (error?.status === 404) return <div>Dashboard não encontrado</div>;
  if (error) return <div>Erro: {error.message}</div>;

  return <div>{/* Renderizar dados */}</div>;
}

A callback onErrorRetry oferece controle fino sobre quando e como retentar. Isso é essencial para lidar com diferentes tipos de erro (4xx vs 5xx) de forma apropriada.

Casos de Uso Reais

Cache compartilhado entre componentes

Um benefício subestimado do SWR é o cache global. Se dois componentes usam a mesma chave, eles compartilham os dados sem requisições duplicadas.

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(res => res.json());

// Componente A
function Header() {
  const { data: user } = useSWR('/api/me', fetcher);
  return <h1>Olá, {user?.name}</h1>;
}

// Componente B
function Sidebar() {
  const { data: user } = useSWR('/api/me', fetcher);
  return <div>Avatar: {user?.avatar}</div>;
}

// Ambos fazem a mesma requisição, mas apenas uma requisição HTTP é feita
export function App() {
  return (
    <div>
      <Header />
      <Sidebar />
    </div>
  );
}

SWR deduplica automaticamente requisições idênticas dentro de um curto período. Isso reduz carga no servidor e melhora performance.

Paginação eficiente

import useSWR from 'swr';
import { useState } from 'react';

const fetcher = (url) => fetch(url).then(res => res.json());

export function UsersList() {
  const [page, setPage] = useState(1);
  const { data, mutate } = useSWR(
    `/api/users?page=${page}&limit=10`,
    fetcher
  );

  const handleNextPage = () => {
    setPage(page + 1);
  };

  const handlePreviousPage = () => {
    if (page > 1) setPage(page - 1);
  };

  if (!data) return <div>Carregando...</div>;

  return (
    <div>
      {data.users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
      <button onClick={handlePreviousPage} disabled={page === 1}>
        Anterior
      </button>
      <button onClick={handleNextPage} disabled={!data.hasMore}>
        Próximo
      </button>
    </div>
  );
}

Cada mudança de page causa uma nova chave no SWR, disparando uma nova requisição. O cache de páginas anteriores permanece intacto, permitindo navegação rápida.

Conclusão

Durante este artigo, cobrimos três pilares fundamentais: primeiro, entender o ciclo stale-while-revalidate não como um conceito abstrato, mas como um fluxo concreto que melhora UX ao servir dados em cache enquanto revalida em background. Segundo, dominar as opções de configuração e o padrão de mutação otimista, transformando SWR de um simples fetcher em uma estratégia completa de gerenciamento de estado assíncrono. Terceiro, reconhecer que SWR resolve problemas reais de deduplição, cache compartilhado e sincronização que você precisaria implementar manualmente com fetch ou axios puro. Com esses conhecimentos, você está preparado para construir aplicações React que não apenas carregam dados, mas que sentem rápidas e responsivas.

Referências


Artigos relacionados