shoc-frontend-new/docs/FRONTEND_MAINTAINABILITY.md
2026-07-24 11:32:58 -03:00

6.2 KiB

Frontend maintainability conventions

Conditional rendering

Use logical && or the when prop on Text when JSX has only a rendered state and an empty state. Use a ternary only when both branches render meaningful alternatives.

{
  error && <Alert severity="error">{error.message}</Alert>;
}

<Text variant="description" when={Boolean(description)}>
  {description}
</Text>;

ESLint rejects condition ? <Element /> : null. This keeps one-sided conditions visually distinct from real either-or UI decisions.

The left operand of && must be entirely boolean. && renders its left operand when it is falsy and non-boolean (notably {count && <X />} renders 0), so a type-aware ESLint rule, seahaven/no-non-boolean-jsx-and, is enforced at error across the repository. It asks the TypeScript checker for the type of the left operand and reports unless every union constituent is boolean-like, so boolean | undefined and string | null fail just as number does. The rule fails closed: if type services are unavailable in a governed TSX file it reports rather than silently claiming safety. The when prop on Text is typed boolean, so TypeScript enforces the same constraint at that component boundary.

Approved guard forms (choose by semantics, not as a blind codemod):

  • Presence-only values where falsy means "absent" — errors, optional strings shown only when set (descriptions, notes, reasons), and optional objects (locations, detail records): coerce with Boolean(value) (or Boolean(a || b) for a composite presence test) before &&. {Boolean(error) && <Alert />} and {Boolean(description) && <Text />} are the canonical forms.
  • Values where 0 or "" is meaningful, or where a non-null value must flow into a typed prop or helper inside the branch: use an explicit nullish/range comparison so the operand is boolean and TypeScript can still narrow. {count > 0 && ...}, {value != null && ...}, and {isEdit && id != null && <Panel vendorId={id} />} keep 0/"" semantics and preserve narrowing.
  • Element-slot props (icon, action, actions) are typed ReactElement, not ReactNode: the slot holds one element (or fragment), and the render branch coerces with Boolean(prop). Do not widen these back to ReactNode, since a slot is never a meaningful 0/"".

Never weaken, disable, baseline, or add per-line exceptions to the rule. A new one-sided condition that needs a non-boolean operand must be rewritten into one of the approved forms above.

Typography and feedback

Use Text from @/components/ui/text for headings, paragraphs, descriptions, labels, captions, code, and asynchronous feedback. It owns:

  • semantic HTML for each visual variant;
  • the display, body, and monospace font families;
  • default, muted, error, success, and warning tones;
  • accessible alert and status live regions for error and feedback text;
  • one-sided conditional text through when.

The variant contract is non-overridable: component, role, and aria-live are omitted from TextProps (TypeScript blocks them) and the remaining props are spread before the variant-owned attributes so the variant element, tone, and live-region role always win at runtime. Use as to choose the rendered element and tone to choose the color; do not pass component, role, or aria-live directly.

Live-region behavior:

  • feedback (polite status) keeps the region mounted and toggles its text content via when, so the polite region exists before its content changes and is announced reliably. Mounting an already-populated status node on demand is not announceable on most screen-reader/browser pairs.
  • error (assertive alert) mounts on demand (when={false} unmounts it). Alert-on-mount is the expected error pattern, so the shipped when={Boolean(error)} usages are correct.

ESLint rejects raw paragraph and heading elements. Existing MUI Typography usages remain valid, but new shared UI should prefer Text so semantics and design tokens do not drift.

The vp-error CSS token is presentational chrome for vendor-portal error cards and may only appear on the Text component. ESLint flags any non-Text JSX element whose className is a static string literal containing vp-error (e.g. <div className="vp-error">, <section className="vp-error extra">); pair the error message with variant="error". The rule enforces the static surface only. It cannot resolve dynamic or composed class values (className={cn("vp-error", ...)}, template literals with expressions, or expression-wrapped strings), so do not compose vp-error dynamically to bypass it — prefer Text variant="error".

Forms and mutations

Use the libraries already established in the application:

  • React Hook Form owns field registration, touched/dirty state, and client form lifecycle.
  • Zod owns form validation and inferred form value types.
  • TanStack Query owns server reads and mutations, including pending/error state, cache invalidation, and retries where safe.

Do not add TanStack Form alongside React Hook Form. It would create two form conventions without removing any current dependency. Reconsider only as a deliberate repository-wide migration with benchmarks, a codemod plan, and an approved deprecation path.

File uploads are not ordinary form fields. Keep file selection and client validation in a focused component, and use a TanStack Query mutation for upload progress, errors, completion refresh, and retry state. Do not place upload orchestration in a route-sized page component.

Page state

Pages should compose focused state components instead of accumulating unrelated booleans:

  • query loading, error, and empty states stay adjacent to the query result;
  • mutation pending/error state belongs to the component that initiated the mutation;
  • route pages coordinate sections and navigation;
  • reusable sections own their interaction details;
  • errors render inline with accessible feedback, with toasts reserved for cross-page outcomes.

Enforcement and rollout

The lint rules are repository-wide and the initial violations were migrated in the same change. npm run lint, npm run build, and the Text behavior tests are required gates. Future maintainability rules must also land with a green migration rather than a warning-only backlog.