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:
- Dados Remotos: Buscar a lista de itens disponíveis em uma API (ex.:
/api/produtos
). - Estado Local: Manter um estado do carrinho (itens adicionados pelo usuário) que não está no servidor até o checkout.
- 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).
- 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.