Skip to content

ADR-007 — React Query como Única Fonte de Cache de Servidor

CampoValor
Status✅ Aceito
Data2026-04-30
AutoresEquipe OLP
Implementaçãosrc/hooks/* (76 hooks), src/contexts/auth-context.tsx, docs/development/REACT_QUERY_CACHE.md

Contexto

Aplicações React tradicionalmente acumulam 3 fontes de "estado de servidor":

  • useState + useEffect chamando fetch (boilerplate, sem invalidação)
  • Redux/Zustand com middleware de fetch (ferramenta errada para o problema)
  • React Query (cache + invalidação + status)

Misturar fontes leva a:

  • Dados desatualizados em uma tela e atualizados em outra
  • "Flash" de dados antigos ao navegar
  • Vazamento cross-school quando usuário multi-papel troca de escola e o cache anterior persiste (incidente real — ver memória query-key-context-isolation)

Decisão

  1. React Query é a única fonte de cache de servidor. Proibido useState([]) + useEffect(fetch) para dados de API.
  2. Toda queryKey de hook escopado por escola DEVE incluir escola_id como primeiro segmento depois do nome:
    ts
    queryKey: ['escola-dashboard', escolaId, page]
    enabled: !!escolaId
  3. Hooks são wrappers tipados sobre useQuery/useMutation — não devem expor instância de queryClient para componentes consumirem (memória react-hooks-factory-restriction).
  4. Toast em onError consome getUserFriendlyError — nunca error.message cru (ADR-009).
  5. Optimistic updates seguem padrão 4-hook (onMutate snapshot → mutação → onError rollback → onSettled invalidate) — memória optimistic-updates-standard.
  6. Safety net cross-school: ao detectar troca de escola_id ou principal_role no applySnapshot do AuthProvider, executar queryClient.removeQueries() antes de hidratar nova sessão. Limpa qualquer cache que tenha sido escrito sem escola_id corretamente.
  7. Estado local continua sendo useState. Estado global de UI (sidebar aberta, modal) continua sendo useState no provider — sem Redux/Zustand.

Alternativas consideradas

A. SWR

Pros: API mais simples. Cons: optimistic updates mais limitados; ecossistema menor; sem mutationFn tão flexível. Veredicto: rejeitado por inferioridade em casos de mutação complexa.

B. RTK Query (Redux)

Pros: integrado ao Redux. Cons: exige Redux como pré-requisito; verboso; não temos Redux. Veredicto: rejeitado.

C. Hooks artesanais com useReducer

Pros: zero dependência. Cons: reinventar invalidação, dedup, refetch on focus → buggy. Já tentado e abandonado. Veredicto: rejeitado.

D. Zustand para tudo

Pros: leve. Cons: ferramenta de estado cliente, não de cache de servidor — sem stale-while-revalidate, sem dedup automático, sem refetch. Veredicto: rejeitado — ferramenta errada (memória state-management-minimalism-principle).

Consequências

Positivas

  • Invalidação determinística: queryClient.invalidateQueries(['gestao-alunos', escolaId])
  • Stale-while-revalidate elimina spinners desnecessários
  • removeQueries() em troca de escola elimina vazamento cross-school (incidente fechado)
  • Devtools React Query torna debug visual

Negativas

  • Curva inicial ("o que é staleTime vs cacheTime?")
  • Esquecer escola_id na queryKey é bug real — cobertura por revisão @audit e memória dedicada
  • Lock-in técnico em React Query

Trade-offs

EixoCustoBenefício
Lock-inMigração custaria reescrever 76 hooksPadrão consistente já internalizado
Convençãoescola_id na key é regra extraMulti-escola seguro by default
Bundle+13KB gzMenos código por hook

Conformidade

Como verificar:

bash
# Componente não deve fazer fetch direto
rg "useEffect.*fetch\(" src --type ts --type tsx
# Hook escopado deve incluir escola_id na queryKey (heurística)
rg "queryKey:\s*\[['\"]" src/hooks --type ts | rg -v "escola"  # revisar manualmente

Sinais de violação:

  • const [data, setData] = useState([]); useEffect(() => fetch(...).then(setData))
  • queryKey: ['minha-key'] sem dimensão de escola em hook escolar
  • setQueryData em componente (deve estar no hook)

Débito técnico conhecido

  • Migração legada de optimistic updates ainda em curso — alguns hooks usam invalidate em vez do padrão 4-hook (TODO inline conforme optimistic-updates-standard).
  • useEscolaDashboard foi normalizado em 2026-04 (incidente cross-school) — outros hooks revisados no mesmo PR.

Referências