Convenções do Frontend
Como o frontend busca dados da API e monta formulários. Cada seção traz a regra e um exemplo real do código.
Queries (React Query)
Toda query é um hook
Cada endpoint lido pelo frontend tem um hook useX(params), num arquivo de mesmo nome dentro
de hooks/queries/:
features/<feature>/hooks/queries/quando só aquela feature consome;shared/hooks/queries/quando mais de uma feature consome.
O componente chama o hook direto no render e usa o resultado como ele vem do useQuery:
// features/variable-income/hooks/queries/useTrades.ts
export function useTrades(ticker: string, { enabled = true }: { enabled?: boolean } = {}) {
return useQuery({
queryKey: queryKeys.trades(ticker),
queryFn: ({ signal }) => apiFetch<Trade[]>(`/api/variable-income/${ticker}/trades`, { signal }),
enabled,
});
}
// features/variable-income/components/order-book-card.tsx
const { data: trades } = useTrades(ticker, { enabled: tab === "HISTORY" });
Os parâmetros do hook são o que varia entre as chamadas: id, ticker, corpo da requisição,
enabled ou um ajuste pontual como o refetchOnMount de useSimulationList.
Chave, tipo e erro
- Chave: sempre pela fábrica
queryKeys(shared/lib/queryKeys.ts). Invalidar um prefixo (queryKeys.statistics()) renova todas as queries debaixo dele. - Tipo da resposta: sempre de
@/types, que reexporta os tipos gerados do OpenAPI. Quando falta um alias, ele nasce emtypes/api.ts. - Erro:
ApiError, registrado como erro padrão do React Query emshared/lib/queryClient.ts. Por issoerrorjá chega tipado em qualqueruseQueryouuseMutation.
Tempo real
O evento que só atualiza a própria query mora dentro do hook dela, com useRealtime +
setQueryData. Todo consumidor do hook recebe o dado novo:
// shared/hooks/queries/usePortfolio.ts
useRealtime("portfolio_update", ({ portfolio }) => {
queryClient.setQueryData(queryKeys.portfolio(), portfolio);
});
O evento que mexe em várias queries, ou que também navega, fica no componente que orquestra
a tela (página ou provider). Exemplo: simulation_ended no SimulationProvider invalida as
estatísticas, troca de rota e renova o status da partida.
Mutations
Mutation segue a mesma regra: um hook useVerboX(params) por arquivo, em hooks/mutations/
(da feature ou de shared/). O componente nunca chama apiFetch direto: toda chamada à API
mora em hooks/queries/ ou hooks/mutations/.
O hook guarda o que vale para qualquer chamada: o mutationFn, o trabalho no cache
(invalidateQueries, setQueryData, update otimista) e o aviso de erro. O que mexe no estado
do componente entra na chamada, como callback do mutate:
// features/variable-income/hooks/mutations/useSubmitOrder.ts
export function useSubmitOrder(ticker: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (order: SubmitOrderRequest) =>
apiFetch<SubmitOrderResponse>(`/api/variable-income/${ticker}/orders`, { method: "POST", body: order }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.portfolioPosition(ticker) }),
onError: (err) => toastApiError(err),
});
}
// features/variable-income/components/order-ticket.tsx
submit.mutate(
{ action, type: kind, quantity, limit_price: limitPrice },
{ onSuccess: () => setQuantityText("") },
);
Formulários
O critério é onde a validação acontece.
React Hook Form + Zod + Field
Quando o formulário tem submit e a validação roda no cliente, campo a campo:
- schema Zod no próprio arquivo do formulário, com as mensagens em português;
useFormcomzodResolver, campos viaController+Field/FieldLabel/FieldErrordeshared/components/ui/field.tsx;- valores iniciais vindos da API chegam já resolvidos pelo componente pai, e entram por
values:oudefaultValues:nouseForm.
Exemplos: features/auth/pages/login.tsx, features/lobby/components/lobby-match.tsx.
useState
Quando a entrada é um filtro, uma busca, um interruptor que salva na hora, uma confirmação por digitação, ou quando quem valida é o servidor:
- a boleta (
features/variable-income/components/order-ticket.tsx) valida a quantidade contra omax_quantityque a prévia da ordem devolve; - a zona de perigo (
features/settings/components/danger-zone-card.tsx) só libera o botão quando o nickname digitado confere.
Testes
O Vitest cobre as contas que o navegador faz: função pura de cálculo ou de formatação, sem componente, tela ou teste ponta a ponta.
- o teste mora ao lado do arquivo testado:
shared/lib/goal.ts→shared/lib/goal.test.ts; - o valor esperado é calculado à mão, com a conta num comentário quando não é óbvia;
describe,iteexpectvêm deimport { ... } from "vitest", e otscconfere os testes junto com o resto;- o Vitest reaproveita o
vite.config.ts(alias@incluído) e roda compnpm run front:test, que também entra nopnpm run check.
// shared/lib/financial-independence.test.ts
it("é a despesa de um ano dividida pela taxa de retirada", () => {
// 12 × 5.000 = 60.000 por ano; a 4%, precisa de 1.500.000
expect(independenceGoal(5000, 0.04)).toBeCloseTo(1_500_000, 6);
});