Gerenciamento de estado global com dependências cruzadas


Abstract

Abaixo um exemplo mostrando uma limitação do SWR em cenários complexos de gerenciamento de estado. A ideia é mostrar uma situação em que o SWR sozinho pode não ser suficiente, exigindo bibliotecas complementares como Redux, Zustand ou Context API para lidar com a complexidade.

Imagine uma aplicação de comércio eletrônico onde você tem uma página de carrinho de compras que precisa lidar com os seguintes requisitos:

  1. Dados Remotos: Buscar a lista de itens disponíveis em uma API (ex.: /api/produtos).
  2. Estado Local: Manter um estado do carrinho (itens adicionados pelo usuário) que não está no servidor até o checkout.
  3. Dependências Cruzadas: Atualizar automaticamente o preço total do carrinho quando:
    • O usuário adiciona ou remove itens.
    • A API retorna uma mudança nos preços dos produtos (ex.: uma promoção).
  4. Sincronização: Garantir que o estado local (carrinho) e os dados remotos (preços) estejam sempre sincronizados.

Tentativa com SWR

Aqui está como você poderia tentar implementar isso usando apenas SWR:

"use client";
import useSWR from "swr";
 
const fetcher = (url) => fetch(url).then((res) => res.json());
 
export default function Carrinho() {
  // Busca os dados dos produtos da API
  const { data: produtos, error, mutate } = useSWR("/api/produtos", fetcher);
 
  // Tentativa de gerenciar o carrinho localmente com useState
  const [carrinho, setCarrinho] = React.useState([]);
 
  // Função para adicionar ao carrinho
  const adicionarAoCarrinho = (produtoId) => {
    const produto = produtos?.find((p) => p.id === produtoId);
    if (produto) {
      setCarrinho((prev) => [...prev, produto]);
    }
  };
 
  // Calcular o preço total
  const precoTotal = carrinho.reduce((total, item) => total + item.preco, 0);
 
  if (error) return <div>Erro ao carregar produtos</div>;
  if (!produtos) return <div>Carregando...</div>;
 
  return (
    <div>
      <h1>Carrinho</h1>
      <ul>
        {carrinho.map((item) => (
          <li key={item.id}>{item.nome} - R$ {item.preco}</li>
        ))}
      </ul>
      <p>Total: R$ {precoTotal}</p>
      <button onClick={() => adicionarAoCarrinho(produtos[0].id)}>
        Adicionar primeiro produto
      </button>
    </div>
  );
}

Problemas com SWR nesse cenário

Estado Local vs Remoto

O SWR é ótimo para buscar e atualizar os produtos da API, mas não gerencia o estado local do carrinho. O useState aqui é independente do SWR, o que cria uma desconexão entre os dados remotos e o estado local.

Se os preços dos produtos mudarem na API (ex.: promoção), o carrinho não reflete isso automaticamente porque ele armazena cópias antigas dos itens.

Sincronização Manual

Para sincronizar o carrinho com os novos preços, você precisaria chamar mutate para atualizar os produtos e então manualmente recalcular o carrinho. Isso é trabalhoso e propenso a erros.

Exemplo:

const atualizarCarrinho = () => {
  mutate("/api/produtos", async () => {
    const novosProdutos = await fetcher("/api/produtos");
    setCarrinho((prev) =>
      prev.map((item) => ({
   	 ...item,
   	 preco: novosProdutos.find((p) => p.id === item.id).preco,
      }))
    );
    return novosProdutos;
  });
};

Dependências Cruzadas

Se outra parte da aplicação (ex.: uma página de recomendações) também modificar o carrinho, o SWR não tem uma forma nativa de manter um estado global consistente entre múltiplos componentes ou páginas.

Escalabilidade

À medida que a aplicação cresce (ex.: filtros no carrinho, cupons de desconto, etc.), gerenciar todas essas interações apenas com SWR e useState fica caótico.

Solução com Biblioteca Complementar

Para resolver isso, você pode combinar o SWR com uma biblioteca de gerenciamento de estado global, como [Zustand], que é leve e simples. O SWR ficaria responsável pelo fetching de dados remotos, enquanto o Zustand gerenciaria o estado do carrinho e as dependências cruzadas.

Exemplo com Zustand

Store com Zustand

// store/carrinhoStore.js
import { create } from "zustand";
 
export const useCarrinhoStore = create((set) => ({
  carrinho: [],
  adicionarAoCarrinho: (produto) =>
    set((state) => ({ carrinho: [...state.carrinho, produto] })),
  limparCarrinho: () => set({ carrinho: [] }),
}));

Componente Revisado

"use client";
import useSWR from "swr";
import { useCarrinhoStore } from "@/store/carrinhoStore";
 
const fetcher = (url) => fetch(url).then((res) => res.json());
 
export default function Carrinho() {
  const { data: produtos, error } = useSWR("/api/produtos", fetcher);
  const { carrinho, adicionarAoCarrinho } = useCarrinhoStore();
 
  // Calcula o preço total com base nos dados mais recentes
  const precoTotal = carrinho.reduce((total, item) => {
    const produtoAtual = produtos?.find((p) => p.id === item.id);
    return total + (produtoAtual ? produtoAtual.preco : item.preco);
  }, 0);
 
  if (error) return <div>Erro ao carregar produtos</div>;
  if (!produtos) return <div>Carregando...</div>;
 
  return (
    <div>
      <h1>Carrinho</h1>
      <ul>
        {carrinho.map((item) => (
          <li key={item.id}>
            {item.nome} - R${" "}
            {produtos.find((p) => p.id === item.id)?.preco || item.preco}
          </li>
        ))}
      </ul>
      <p>Total: R$ {precoTotal}</p>
      <button onClick={() => adicionarAoCarrinho(produtos[0])}>
        Adicionar primeiro produto
      </button>
    </div>
  );
}

Vantagens dessa Abordagem

  • Separação de Responsabilidades: SWR cuida do fetching e caching dos produtos, enquanto Zustand gerencia o estado global do carrinho.
  • Sincronização Automática: O preço total reflete os dados mais recentes da API sem manipulação manual complexa.
  • Escalabilidade: O Zustand pode ser usado em outras páginas ou componentes sem duplicação de lógica.

Por que Isso Mostra a Limitação do SWR?

O SWR é projetado para fetching de dados e caching, não para gerenciamento de estado global ou dependências complexas entre dados locais e remotos.

Quando você precisa de algo além de buscar e revalidar dados (como manter um carrinho sincronizado com preços dinâmicos ou compartilhar estado entre componentes), ele começa a exigir soluções manuais ou bibliotecas complementares.

Referências


https://github.com/pmndrs/zustand