Aller au contenu

Data fetching & admin API errors

appQueryClient, processHttpResult validation, the admin-API error pipeline driving the global toast and modal submit wrappers. Keep in sync: changing appQueryClient defaults, MutationCache.onError, processHttpResult, withGlobalErrorHandling, useSubmitWithError, or the error-code flow → update this file in the same PR.

Hard rules

  • Single appQueryClient from @bricks-common/bo/core, provided at app root. Don't instantiate a parallel client.
  • Router context exposes the QueryClient. __root.tsx uses createRootRouteWithContext<{ queryClient: QueryClient }>. Loader prefetch reads it from Route.useRouteContext() — never useEffect-fetch on mount.
  • Defaults: staleTime: 0, gcTime: 5 days, retry: 0 (queries + mutations). Override per call site only when the operation genuinely needs to retry.
  • Services just throw. No try/catch on a specific code, no 'available' | 'unavailable' discriminated returns, no *_INELIGIBLE_CODE constants. Let the error propagate; the global handler toasts.
  • Don't reach into error.response.data?.message / ?.code. useSubmitWithError + buildAdminApiErrorMessage cover every case.
  • Error code translations live in api-communication-bricksoffice, not the consumer app. src/translations/fr.json under error-api.<code>.
  • Request payloads + shared primitives single-sourced in @bricks-common/api-communication-bricksoffice — the API imports them too (BRI-782). Don't re-declare in a controller or in the app. Stricter server guard → strengthen the shared validator (same output type), don't fork. Status: api-communication-bricksoffice/src/VALIDATOR-MUTUALIZATION.md.
  • HTTP client via context. <HttpClientProvider client={adminAxios}> at the consumer root; services read useHttpClient(). No axios prop drilling, no module-scope singleton import.
  • Gate project-scoped queries with enabled, never skipToken alone. With staleTime: 0, an unconditional fetch on a globally-mounted host refetches on every window focus, so id-gated / always-mounted queries must be gated. But skipToken doesn't make a query inactive, and invalidateProjectScopedQueries invalidates every <module>Keys.all (the bare prefix an id-gated hook falls back to when its id is null) — so a lone skipToken query gets force-refetched and throws "Attempted to invoke queryFn when set to skipToken". Always pair them: queryFn: id != null ? () => … : skipToken (types the queryFn for the absent-id branch, kills focus-refetch) and enabled: id != null (keeps the disabled query inactive so the broad invalidate skips it). Applies to id-gated hooks (canonical useSpvLemonwayBalance) and shared keys alike (projectKeys.list() via useProjects).

The error pipeline

Admin API returns { statusCode, message: "<kebab-code>", … } on every 4xx/5xx — message is the code, not text. Codes translate via adminTranslations['error-api'][code] from @bricks-common/api-communication-bricksoffice; fall back to raw code, then errors.generic.

Wired three ways: - Mutations toast via the global MutationCache.onError on appQueryClient. - Queries going through processHttpResult toast the same (default showErrorNotification: true). - Modal submits wrap with withGlobalErrorHandling (default) or useSubmitWithError.

Helpers from @bricks-common/bo/core

Helper Use for
appQueryClient the single QueryClient
processHttpResult({ responsePromise, validator }) unwrap axios + validate via idtlt; toasts on error by default
withGlobalErrorHandling(asyncFn) wrap a modal submit; swallows rejections so success doesn't run on error
useSubmitWithError() hook variant — { handleSubmit, error, clearError }; reach for it for error.code branching or inline <Alert>
buildAdminApiErrorMessage(error) translate axios error → French (used internally by the global handler)
getAdminApiErrorCode(error) raw kebab code (when branching)

HttpClientProvider / useHttpClient() are at @bricks-common/bo/providers.

Service + mutation pattern

// services/useProject.ts
export const useProject = (id: UUID) => {
  const httpClient = useHttpClient()
  return useQuery({
    queryKey: projectsKeys.detail(id),
    queryFn: (): Promise<ProjectResponse> =>
      processHttpResult({
        responsePromise: httpClient.get(getProjectEndpoint.request.path({ id })),
        validator: projectResponse,
      }),
  })
}

// services/queryKeys.ts — centralized per module
export const projectsKeys = {
  all: ['projects'] as const,
  list: () => [...projectsKeys.all, 'list'] as const,
  detail: (id: UUID) => [...projectsKeys.all, 'detail', id] as const,
}

Mutations always invalidate the list on success.

Project-scoped mutations: invalidateProjectScopedQueries

Why blanket, not per-id precise? Denormalized fields (hasInternalNote, payment status, balance, overdue counts) surface across 6+ tables. We shipped the same stale-data bug repeatedly when invalidating only the home module. Broad invalidation costs a few refetches; under-invalidating shows wrong data.

import { invalidateProjectScopedQueries } from '@/modules/projects/services/invalidations'

onSuccess: () => invalidateProjectScopedQueries(queryClient)

Adding a new project-touching module → extend the helper's list in invalidations.ts. Don't call extra invalidateQueries at each mutation's call site.

Non-project mutations (useCreatePropertyMonthlyUpdate, useUploadYearlyFinancialUpdate, …) keep narrow scoped invalidations.

// Default
const handleSubmit = withGlobalErrorHandling(async (values) => {
  await mutation.mutateAsync(...)
  notifySuccess(m['...']())
  onClose()
})

// When you need the error locally (inline alert, branch on code)
const { handleSubmit, error, clearError } = useSubmitWithError()

For query-level branching (a query error gates UI like a disabled form), prefer query.isError. Inspect the code only when UX needs to distinguish reasons.

For mutations needing an inline error instead of a toast, opt out with useMutation({ meta: { silent: true } }) and render from error.message.

Adding a new endpoint

  1. Add it in projects/common/both/api-communication-bricksoffice/src/endpoints/.
  2. Re-export from the package barrel.
  3. Add matching error-api.<code> translations to src/translations/fr.json — otherwise the toast falls back to the raw kebab.
  4. Rebuild the package (pnpm --filter @bricks-common/api-communication-bricksoffice build) before the consumer resolves the new exports.

What NOT to do

  • try { ... } catch { /* global handler */ } inline in a modal submit — use withGlobalErrorHandling.
  • try/catch in a service to translate a business code into null / 'unavailable' — let it propagate.
  • status >= 400 && < 500 catch-all to mean "ineligible" — swallows 401/403/404/422 and hides real failures.
  • notifyError(buildAdminApiErrorMessage(error)) from a mutation catch — global handler already toasts; you'd double-toast.
  • ❌ Hand-list invalidateQueries({ queryKey: <module>Keys.all }) in a project-scoped mutation's onSuccess — extend invalidateProjectScopedQueries instead.