TypeScript Coding Guidelines (React Frontend)¶
The primary, thorough guideline for the frontend-react profile. Builds on three other guidelines:
- JS/TS Coding Guidelines — TypeScript strictness, module style, async/error handling, immutability, testing basics, security fundamentals (not repeated here)
- General Coding Guidelines
- UI Architecture Guidelines — the Domain Model → Presentation Model → View Model → View layering this file maps onto React specifically
Project Layering¶
This profile follows the app → infra → usecase → domain dependency direction
(see general_coding.md), under src/:
domain/— pure business entities and value objects. No React import, nofetch, no browser API. Exactly the "Domain Model" ofui_architecture.md.usecase/— application logic operating on the domain, plus port interfaces (e.g. anAuthProviderport — pergeneral_coding.md, output-port interfaces belong inusecase, notdomain) and the Presentation-Model/View-Model construction logic for a screen.infra/— concrete port implementations: the real API client and its DTO-to-domain mapping (see below),NoopAuthProvider(and any later real auth adapter), and MSW mock handlers.app/— the View layer: components, pages, routing, and the composition root that wiresinfraimplementations intousecases.
Applying MVVM with React Hooks¶
ui_architecture.md describes Presentation Model (PM) and View Model (VM) as "plain objects,"
independent of any UI framework. In React, the natural equivalent of a plain, framework-agnostic
object with reactive properties is a custom hook:
- A PM hook (e.g.
useUserProfilePresentationModel(user: User)) takes domain data and returns display-ready values — formatted strings, filtered/sorted lists — with zero widget-state fields. - A VM hook (e.g.
useLoginFormViewModel(pm: LoginPresentationModel)) returns only widget control state:isSubmitEnabled,isSpinnerVisible,hasEmailError— booleans and enums, never data. - The View is a plain function component that calls these hooks and renders their return values — zero branching logic in the component body beyond what JSX itself requires for conditional rendering driven by VM flags.
- PM/VM hooks are tested with
@testing-library/react-hooks-style rendering (or plain function calls, if the hook has no internaluseState/useEffect) — no full component render needed.
// usecase/profile/useUserProfilePresentationModel.ts — Presentation Model
export function useUserProfilePresentationModel(user: User) {
return {
displayName: user.fullName.toString(),
joinedLabel: formatRelativeDate(user.joinedAt),
};
}
// app/profile/UserProfileView.tsx — View: no logic beyond binding
export function UserProfileView({ user }: { user: User }) {
const pm = useUserProfilePresentationModel(user);
return (
<section data-testid="user-profile">
<h1 data-testid="user-profile-name">{pm.displayName}</h1>
<p data-testid="user-profile-joined">{pm.joinedLabel}</p>
</section>
);
}
DTO-to-Domain Mapping¶
The API is an external boundary like any other. This mirrors the infra→usecase boundary
pattern in error_handling.md and the DDD alignment in data_modeling.md. infra/ receives
raw DTOs matching the API/OpenAPI contract and maps them into domain/ entities/value objects
before usecase/ sees them.
- Define DTO types next to the API client in
infra/api/— they mirror the wire format exactly, including fields the domain doesn't need. - Write one mapping function per DTO (
toUser(dto: UserDto): User), even when a field is a pure passthrough — the mapping seam is what absorbs a future API contract change without rippling into domain logic or views. - Never pass a DTO type into
usecase/ordomain/— if a function signature in those layers accepts a*Dtotype, the mapping boundary has been skipped.
React Hooks Rules¶
- Never call a hook conditionally, in a loop, or after an early return — hook call order must be identical on every render.
- Declare every reactive value a
useEffect/useMemo/useCallbackreads in its dependency array; do not suppress the linter rule instead of fixing the dependency. - One responsibility per custom hook; a hook that returns both PM-shaped and VM-shaped values should be split into two.
Stable Test Hooks¶
- Every interactive element and every piece of content a test might assert on gets a
data-testid, named<feature>-<element>in kebab-case (e.g.login-submit-button,user-profile-name) — see the example above. - Never target elements in tests by CSS class, tag name, or DOM position — see Responsive Web Guidelines.
Testing with Mock Service Worker (MSW)¶
- Define MSW request handlers once in
infra/api/mocks/handlers.ts; reuse them in component tests (Vitest + React Testing Library), in local dev mode (infra/api/mocks/browser.ts), and in mocked E2E runs (infra/api/mocks/server.tsfor Node/Playwright) — one source of truth for fake API responses, not three. - Component tests assert on rendered output and
data-testidqueries, never on internal hook state.
Error Handling¶
- A custom
Errorsubclass exposes its data asreadonlyconstructor properties (e.g.readonly bookingId: string), not only inside.message— seeerror_handling.mdandjs_coding.mdfor the cross-language rationale.
Directives¶
- Follow
src/{domain,usecase,infra,app}layering - Presentation Model and View Model are custom hooks with zero UI framework leakage; View components bind only, no branching logic
- Map API DTOs to domain entities/value objects at the infra→usecase boundary — never pass a DTO type into usecase or domain
- Never call hooks conditionally or in a loop; declare every effect/memo dependency explicitly
- Every test-targeted element has a
data-testidin<feature>-<element>kebab-case form - Define MSW handlers once in
infra/api/mocks/, reused across component tests, dev mode, and mocked E2E - Custom
Errorsubclasses expose their data asreadonlyconstructor properties, not only inside.message