21 — Catálogo de hooks 21 — Catálogo de hooks Os hooks reutilizáveis que todo projeto web deste padrão deve ter em src/hooks/. São agnósticos de domínio: nenhum conhece Fornecedor, Documento ou qualquer entidade. Copie os doze arquivos deste documento no bootstrap — antes da primeira tela, não depois. Inventário Hook Camada Depende de Documento useConnectQuery Sessão api/session.connect, STORAGEKEY 05 useLoginMutation Sessão api/session.login, Session 05 useAuthQuery Sessão os dois acima + logoutSession 05 mutationMessages Caso de uso NotificationProvider 06 useCurioMutation Caso de uso @curio/client/react, mutationMessages 06 useUseCaseControls Caso de uso idem + isAuthError + useCaseTriggersLogger 06 useSearcher Caso de uso AuthProvider 06 useHookMutation Caso de uso mutationMessages 09 useTabCloseCallback Abas TabsContext, CurrentTabContext 09 useValidatedForm Formulário react-hook-form, zod 11 useDebounce Utilitário nada — useLocalStorage Utilitário nada — Mais um arquivo de apoio: src/utils/useCaseTriggersLogger.ts, exigido por useUseCaseControls. mutationMessages.ts não é um hook de tela — é o núcleo compartilhado entre useCurioMutation e useHookMutation (ver abaixo). Copie-o junto dos dois. Grafo de dependências useAuthQuery ─┬─ useConnectQuery ── api/session.connect └─ useLoginMutation ── api/session.login └─ (logout) ── api/session.logoutSession mutationMessages ── NotificationProvider (useNotifyMutationSuccess + tipo MutationMessages) useCurioMutation ─┬─ UseCaseManager (@curio/client/react) └─ mutationMessages useUseCaseControls ─┬─ UseCaseManager ├─ NotificationProvider └─ utils/useCaseTriggersLogger useSearcher ──── AuthProvider (session direta, sem UseCaseManager) useHookMutation ──── mutationMessages (funcao async qualquer) useTabCloseCallback ─┬─ TabsContext └─ CurrentTabContext useValidatedForm ── react-hook-form + zod useDebounce, useLocalStorage ── nada Ordem de cópia: utils/useCaseTriggersLogger.ts e mutationMessages.ts primeiro (nada depende deles e várias coisas dependem deles), depois os demais hooks. useAuthQuery por último — ele importa os outros dois de sessão. Barrel // src/hooks/index.ts // Sessao e autenticacao export { useAuthQuery } from "./useAuthQuery"; export { useConnectQuery } from "./useConnectQuery"; export { useLoginMutation } from "./useLoginMutation"; // Caso de uso Curio export { useCurioMutation } from "./useCurioMutation"; export { useUseCaseControls } from "./useUseCaseControls"; export { useSearcher } from "./useSearcher"; export { useHookMutation } from "./useHookMutation"; export { useNotifyMutationSuccess, type MutationMessages } from "./mutationMessages"; // Abas export { useTabCloseCallback } from "./useTabCloseCallback"; // Formularios export { useValidatedForm } from "./useValidatedForm"; // Utilitarios export { default as useDebounce } from "./useDebounce"; export { default as useLocalStorage } from "./useLocalStorage"; Note a mistura de export nomeado e default: useDebounce e useLocalStorage usam export default; os demais, export nomeado. É herança da referência. Num projeto novo, padronize em export nomeado — o barrel fica uniforme e o rename fica rastreável. Camada de sessão Os três hooks de sessão formam uma composição. AuthProvider consome apenas useAuthQuery; página nenhuma toca nos outros dois. useConnectQuery Reconecta a partir do token guardado, no boot e no refresh de página. const connectQuery = useConnectQuery(); Três decisões embutidas: enabled: hasToken — sem token, a query nem roda. Evita request inútil no primeiro acesso. Timeout de 8s via Promise.race — connect() não tem timeout próprio; sem isso a tela fica em loading indefinido quando o servidor não responde. Os throw explícitos — connect() retorna o erro em vez de lançar (05). Este hook é a fronteira que converte isso no que o React Query espera. useLoginMutation const loginMutation = useLoginMutation(); await loginMutation.mutateAsync({ email, password }); Grava o token no sessionStorage e invalida ["auth", "connect"], mantendo um único estado de sessão. useAuthQuery Compõe os dois e expõe a API que o AuthProvider usa: const { session, isAuth, isLoading, login, logout, refetchConnection } = useAuthQuery(); Campo O que é session Session ativa, ou undefined isAuth !!session isLoading Reconectando ou autenticando login (params) => Promise logout Aborta no servidor, reseta e limpa o cache refetchConnection Força a reconexão — usado no boot pelo AuthProvider isConnecting / isLoggingIn / connectError / loginError Estado granular, para telas de login A sessão vem de loginMutation.data ?? connectQuery.data: um login recém-feito tem precedência sobre a reconexão. O logout precisa chamar logoutSession(session). Limpar sessionStorage e o cache não encerra a sessão no servidor — só session.abort() faz isso. É comum uma implementação de useAuthQuery omitir essa chamada e deixar sessão órfã no backend; a versão deste catálogo corrige. Camada de caso de uso Núcleo compartilhado: mutationMessages.ts useCurioMutation e useHookMutation são, na maior parte, o mesmo hook: um useMutation que aceita msgSucesso/msgErro/msgErroFallback e dispara a notificação de sucesso do mesmo jeito. A diferença real entre os dois está só em como msgErro/msgErroFallback chegam ao mutationCache.onError global — porque a forma de TParams é diferente: useCurioMutation: TParams é sempre um objeto (o payload do backend), então as mensagens cabem dentro dele — viajam por variables. useHookMutation: TParams é o valor que a função recebe (uma Session, void, o que for), não necessariamente um objeto — as mensagens não cabem ali. Viajam pelo meta da mutation, que o React Query aceita justamente para isto. O que é idêntico foi extraído para um arquivo à parte, e os dois hooks reutilizam: // src/hooks/mutationMessages.ts import { useCallback } from "react"; import { useNotification } from "@context/NotificationProvider"; export interface MutationMessages { msgSucesso?: string; /** Sobrescreve qualquer mensagem de erro do backend — sempre prevalece. */ msgErro?: string; /** Usada só quando o backend não devolve mensagem e msgErro não foi definido. */ msgErroFallback?: string; } // unico ponto que dispara a notificacao de sucesso — compartilhado por todo hook de mutation export function useNotifyMutationSuccess() { const { setNotification } = useNotification(); return useCallback( (msgSucesso?: string) => { if (msgSucesso) setNotification({ type: "success", message: msgSucesso }); }, [setNotification] ); } queryClient.ts também importa só o tipo MutationMessages (import type, sem custo em runtime) para ler msgErro/msgErroFallback de variables ou de mutation.meta no mesmo lugar — ver 07. Não force os dois hooks a usar o mesmo mecanismo de transporte (variables vs meta). A diferença não é acidental — é consequência de TParams ter formas diferentes. Force-fit aqui pioraria os dois lados: useCurioMutation perderia a possibilidade de variar msgErro por chamada, ou useHookMutation teria que envolver todo TParams num objeto só para caber uma mensagem. useCurioMutation O mais importante do conjunto. Envia um request nomeado dentro do caso de uso aberto pelo UseCaseManager. export const useSalvaFornecedor = () => useCurioMutation("RM_SALVA_OBJETO"); const { mutateAsync: salvar, isPending } = useSalvaFornecedor(); await salvar({ Fornecedor: { _OID: "123", _Nome: "ACME" }, msgSucesso: "Fornecedor salvo com sucesso!", onSuccess: () => setModalAberto(true) }); Quatro coisas que ele faz e o useMutation cru não faria: msgSucesso, msgErro, msgErroFallback viram notificação e são removidos do payload antes de ir ao backend (delete params.msgSucesso, etc.). A mensagem de erro segue prioridade fixa, resolvida no mutationCache.onError global (07), não dentro deste hook: msgErro (sempre prevalece) → mensagem do backend → msgErroFallback → fallback genérico interno. useCurioMutation não tem onError próprio — se tivesse, a notificação apareceria duas vezes. Callbacks no mesmo objeto dos parâmetros — onSuccess, onError, onSettled, onMutate convivem com os params num argumento só. Difere do React Query puro, onde seriam um segundo argumento. Tipagem condicional em TParams — quando é void, o argumento inteiro fica opcional (incluirFornecedor()); quando não é, fica obrigatório. Use isPending, não isLoading — em mutations da v4.36 o isLoading está deprecado. useUseCaseControls Abre e fecha o caso de uso, e expõe o status. const { open, close, status, error } = useUseCaseControls(); status vai de "idle" a "open". O open devolvido é a versão segura: o open() do Curio lança, e este converte em notificação. Descobrir os requests disponíveis: const { open, status } = useUseCaseControls({ enableLogs: true }); Loga no console, só em desenvolvimento, os RMs chamáveis no estado atual. Remova antes de commitar — o hook check-debug-flags.sh do pré-commit barra enableLogs: true (14). Exige src/utils/useCaseTriggersLogger.ts. useSearcher Busca genérica pelas operações 120 (buscar) e 134 (obter contexto), sem UseCaseManager. const { getContext, search } = useSearcher("465"); const resultado = await search.mutateAsync({ _Nome: "ACME", _Ativo: true }); Usa a sessão do useAuth() diretamente. Escolha entre os dois caminhos: Situação Use Tela só de filtro + lista, sem estado no servidor useSearcher Incluir, alterar, salvar — há estado no caso de uso UseCaseManager + useCurioMutation useHookMutation Roda uma função async qualquer como mutation — para ação que não passa por UseCaseManager. O caso canônico é o action de um item de menu (09). // src/hooks/useHookMutation.ts import { useMutation } from "@tanstack/react-query"; import { MutationMessages, useNotifyMutationSuccess } from "./mutationMessages"; export const useHookMutation = ( func: (params: TParams) => Promise, messages: MutationMessages = {} ) => { const notifySuccess = useNotifyMutationSuccess(); const { msgSucesso, msgErro, msgErroFallback } = messages; return useMutation({ mutationFn: (params) => func(params), meta: { msgErro, msgErroFallback }, // TParams nao e' objeto — nao cabe em variables onSuccess: () => notifySuccess(msgSucesso) }); }; const { mutateAsync: executar, isPending } = useHookMutation(handleAbrirRelatorio, { msgSucesso: "Relatório gerado.", msgErroFallback: "Não foi possível gerar o relatório." }); await executar(session); Diferença para useCurioMutation: aquele envia um request dentro de um caso de uso já aberto; este executa uma função que abre e fecha o próprio caso de uso. Ambos reutilizam MutationMessages/useNotifyMutationSuccess de mutationMessages.ts — ver "Núcleo compartilhado" acima. Assinatura divergente de um padrão comum. É comum ver msgSucesso/msgErro dentro dos parâmetros ({ options: { ... } }) com o parâmetro tipado como unknown. Isso quebra quando o parâmetro é uma instância de classe como Session, e obriga a um cast em cada nó do menu. Aqui as mensagens são argumento de criação do hook e TParams é genérico de verdade. Abas useTabCloseCallback Registra o que rodar quando a aba da página for fechada. const { close } = useUseCaseControls(); useTabCloseCallback(close); Existe porque abas ficam montadas o tempo todo — só o painel ativo é visível. Logo, cleanup de useEffect não dispara ao fechar a aba. Sem este hook, o caso de uso fica aberto no servidor depois que o usuário fecha a aba. É no-op quando a página veio pelo da rota, então a mesma página serve aos dois modos de navegação sem if. Formulário e utilitários useValidatedForm const form = useValidatedForm({ schema: fornecedorSchema, defaultValues: { _Nome: "", _Ativo: true } }); Garante zodResolver e mode: "onChange" uniformes. Nunca use useForm direto — ver 11. useDebounce const termoDebounced = useDebounce(termo, 400); Atrasa a propagação de um valor. Use em filtro que dispara busca a cada tecla. useLocalStorage const [colunas, setColunas, removeColunas] = useLocalStorage("tabela.colunas", colunasPadrao); Valor JSON no localStorage, sincronizado entre abas — dispara um CustomEvent próprio porque o evento storage nativo não chega à aba que escreveu. Não use para token de sessão. Isso é responsabilidade de lib/curio + api/session, que usam sessionStorage (05). O que não copiar de uma implementação existente É comum achar hooks em src/hooks/ que parecem genéricos mas não pertencem a este catálogo: Sinal Por quê Nome ligado a uma tela específica (ex.: useDashboard) Casos de uso do domínio daquele projeto. Específico Envelopa useState booleano só para renomear (ex.: useModal) Abstração fina demais para justificar existir Verificação Depois de copiar: npx tsc --noEmit && npm run lint Erros que aparecem quando falta uma peça: Erro Falta Cannot find module '@utils/useCaseTriggersLogger' O arquivo de apoio, ou o alias @utils Cannot find module '@curio/client/react' Versão do @curio/client sem entrada react Property 'isPending' does not exist React Query v4 antigo — exige 4.36+ useNotification precisa estar dentro de... NotificationProvider fora do lugar em main.tsx (03) Os hooks de sessão só se provam com backend real: faça login, dê F5 (deve manter a sessão) e faça logout (deve voltar ao login). Os de caso de uso só se provam na primeira tela (17). Confira sempre contra este catálogo antes de copiar um hook de outra implementação — o defeito de logout descrito acima é um erro recorrente em versões de useAuthQuery.