ADR-007 — React Query como Única Fonte de Cache de Servidor
| Campo | Valor |
|---|---|
| Status | ✅ Aceito |
| Data | 2026-04-30 |
| Autores | Equipe OLP |
| Implementação | src/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+useEffectchamandofetch(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
- React Query é a única fonte de cache de servidor. Proibido
useState([]) + useEffect(fetch)para dados de API. - Toda
queryKeyde hook escopado por escola DEVE incluirescola_idcomo primeiro segmento depois do nome:tsqueryKey: ['escola-dashboard', escolaId, page] enabled: !!escolaId - Hooks são wrappers tipados sobre
useQuery/useMutation— não devem expor instância dequeryClientpara componentes consumirem (memóriareact-hooks-factory-restriction). - Toast em
onErrorconsomegetUserFriendlyError— nuncaerror.messagecru (ADR-009). - Optimistic updates seguem padrão 4-hook (
onMutatesnapshot → mutação →onErrorrollback →onSettledinvalidate) — memóriaoptimistic-updates-standard. - Safety net cross-school: ao detectar troca de
escola_idouprincipal_rolenoapplySnapshotdoAuthProvider, executarqueryClient.removeQueries()antes de hidratar nova sessão. Limpa qualquer cache que tenha sido escrito semescola_idcorretamente. - Estado local continua sendo
useState. Estado global de UI (sidebar aberta, modal) continua sendouseStateno 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_idna queryKey é bug real — cobertura por revisão@audite memória dedicada - Lock-in técnico em React Query
Trade-offs
| Eixo | Custo | Benefício |
|---|---|---|
| Lock-in | Migração custaria reescrever 76 hooks | Padrão consistente já internalizado |
| Convenção | escola_id na key é regra extra | Multi-escola seguro by default |
| Bundle | +13KB gz | Menos código por hook |
Conformidade
Como verificar:
# 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 manualmenteSinais de violação:
const [data, setData] = useState([]); useEffect(() => fetch(...).then(setData))queryKey: ['minha-key']sem dimensão de escola em hook escolarsetQueryDataem componente (deve estar no hook)
Débito técnico conhecido
- Migração legada de optimistic updates ainda em curso — alguns hooks usam
invalidateem vez do padrão 4-hook (TODO inline conformeoptimistic-updates-standard). useEscolaDashboardfoi normalizado em 2026-04 (incidente cross-school) — outros hooks revisados no mesmo PR.
Referências
docs/development/REACT_QUERY_CACHE.mddocs/development/NEW_HOOK.mdsrc/contexts/auth-context.tsx(safety net)mem://architecture/react-query-and-toast-standardization-v2-0mem://architecture/query-key-context-isolationmem://architecture/optimistic-updates-standard- ADR-004 —
mutationFnconsomeinvokeAction - ADR-009 —
onErrorconsome - ADR-010 —
removeQueriesem troca de papel