Frontend Rule
Core rules for frontend TypeScript and React development
Frontend
Guidelines for frontend TypeScript and React development, including component structure, code style, architecture patterns, and build/format steps.
Code Navigation
Use LSP tools aggressively for code investigation: goToDefinition, findReferences, hover, documentSymbol. If LSP returns "No LSP server available", stop and instruct the user: npm install -g typescript-language-server typescript
Browser Testing
Use browser MCP tools to test at https://localhost:9000. Use UNLOCK as OTP verification code (localhost only). Use run MCP tool to restart the server if needed (wait a few seconds after restart).
Architecture Overview
-
SPA Served by .NET Backend:
- SPA served via
SinglePageAppFallbackExtensions.csfrom the backend - UserInfo injected into HTML meta tags and available via
import.meta.user_info_env - Authentication is server-side with HTTP-only cookies
- YARP reverse proxy handles routing between SPA and APIs
- SPA served via
-
Module Federation for Micro-Frontends:
- Each self-contained system has its own WebApp
- Common UI exposed via federation in
federated-modules/ - Shared components in
application/shared-webapp/ - Don't import directly between self-contained systems
- Use
window.location.hreffor navigation between systems (not TanStack Router)
-
API Integration:
- API client auto-generated from OpenAPI spec
- Located in
shared/lib/api/client.ts - Never make direct fetch calls
- Server state lives in TanStack Query only
- Use
queryClient.invalidateQueries()to refresh data after mutations
Implementation
-
Follow these code style and pattern conventions:
- Use proper naming conventions:
- PascalCase for components (e.g.,
UserProfile,NavigationMenu) - camelCase for variables and functions (e.g.,
userName,handleSubmit)
- PascalCase for components (e.g.,
- Create semantically correct components with clear boundaries and responsibilities:
- Each component should have a single, well-defined purpose
- UI elements with different functionality should be in separate components
- Avoid mixing unrelated functionality in one component
- Use clear, descriptive names instead of making comments
- Don't use acronyms (e.g., use
errorMessagenoterrMsg,buttonnotbtn,authenticationnotauth) - Prioritize code readability and maintainability
- Don't introduce new npm dependencies
- Use React Aria Components instead of native HTML elements like
<a>,<button>,<fieldset>,<form>,<h1>-<h6>,<img>,<input>,<label>,<ol>,<p>,<progress>,<select>,<table>,<textarea>,<ul>(native<div>,<span>,<section>,<article>are acceptable)
- Use proper naming conventions:
-
Use the following React patterns and libraries:
- Use React Aria Components from
@repo/ui/components/ComponentName:- Search Components when you need to find a component
- Use existing components rather than creating new ones
- Use
onPressinstead ofonClickfor event handlers (exception: Dialog close button usesonClick={close}from render prop) - Use
onActionfor menu items and list actions - Use
<Trans>...</Trans>for JSX translations,tmacro for strings - Use TanStack Query for API interactions via
api.useQuery()andapi.useMutation() - Don't use
fetchdirectly—use the generated API client - Use Suspense boundaries with error boundaries at route level
- Colocate state with components—don't lift state unnecessarily
- Use
useCallbackanduseMemoonly for proven performance issues - Throw errors sparingly and ensure error messages include a period
- Include appropriate aria labels for accessibility (e.g.,
slot="title"on Heading in dialogs) - Disable UI during pending operations:
isDisabled={mutation.isPending}on buttons/fields,isDismissable={!mutation.isPending}on modals - Dialog sizing:
sm:w-dialog-md(simple),sm:w-dialog-lg(4-6 fields),sm:w-dialog-xl(complex),sm:w-dialog-2xl(extra-large)
- Use React Aria Components from
-
Error handling:
- Errors are handled globally—
shared-webapp/infrastructure/http/errorHandler.tsautomatically shows toast notifications with the server's error message (don't manually show toasts for errors) - Validation errors: Pass to forms via
validationErrors={mutation.error?.errors} onErroris for UI cleanup only (resetting loading states, closing dialogs), not for showing errors- Toast notifications: Show success toasts in mutation
onSuccesscallbacks, not inuseEffectwatchingisSuccess(avoids React effect scheduling delays)
- Errors are handled globally—
-
Responsive design utilities:
- Use
useViewportResize()hook to detect mobile viewport (returnstruewhen mobile) - Use
isTouchDevice()for touch vs mouse interactions - Use
isMediumViewportOrLarger()for desktop-specific features
- Use
-
Z-index layering for fixed-position elements (don't invent new values):
z-0toz-20: Content layers (sticky headers, table headers)z-30toz-40: Navigation (top bar, mobile header)z-60: Side menu collapsedz-70: Side panes (backdrop atz-[65])z-80: Side menu expanded in overlay mode (backdrop atz-[75])z-90: Modal dialogsz-100: High priority modals (nested, confirmations)z-[150]: Toasts (always visible for user feedback)z-[200]: Mobile full-screen menus- Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context
-
DirtyModal close handlers:
- X button: Use Dialog's
closefrom render prop (shows unsaved warning if dirty) - Cancel button: Use
handleCancelthat clears state and closes immediately (bypasses warning) - Always clear dirty state in
onSuccessandonCloseComplete
- X button: Use Dialog's
-
Always follow these steps when implementing changes:
- Consult relevant rule files and list which ones guided your implementation
- Search the codebase for similar code before implementing new code
- Reference existing implementations to maintain consistency
-
Build and format your changes:
- After each minor change, use the execute MCP tool with
command: "build"for frontend - This ensures consistent code style across the codebase
- After each minor change, use the execute MCP tool with
-
Verify your changes:
- When a feature is complete, run these MCP tools for frontend in sequence: build, format, inspect
- ALL inspect findings are blocking - CI pipeline fails on any result marked "Issues found"
- Severity level (note/warning/error) is irrelevant - fix all findings before proceeding
- Fix any compiler warnings or test failures before proceeding
Examples
// ✅ DO: Correct patterns
export function UserPicker({ isOpen, onOpenChange }: UserPickerProps) {
const [isFormDirty, setIsFormDirty] = useState(false);
const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen });
const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline
const inviteMutation = api.useMutation("post", "/api/account-management/users/invite", {
onSuccess: () => { // ✅ Show toast in onSuccess (not useEffect)
setIsFormDirty(false);
toastQueue.add({ title: t`Success`, description: t`User invited`, variant: "success" });
onOpenChange(false);
}
});
const handleCloseComplete = () => setIsFormDirty(false);
const handleCancel = () => { setIsFormDirty(false); onOpenChange(false); }; // ✅ Clear state + close (bypasses warning)
return (
<DirtyModal isOpen={isOpen} onOpenChange={onOpenChange} hasUnsavedChanges={isFormDirty}
isDismissable={!inviteMutation.isPending} onCloseComplete={handleCloseComplete}>
<Dialog className="sm:w-dialog-md"> // ✅ Use dialog width classes (not max-w-lg)
{({ close }) => ( // ✅ Dialog render prop provides close function
<>
<XIcon onClick={close} className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" /> // ✅ X uses close (shows warning if dirty)
<DialogHeader description={t`Select users from the list.`}>
<Heading slot="title" className="text-2xl"><Trans>Select users</Trans></Heading>
</DialogHeader>
<Form onSubmit={mutationSubmitter(inviteMutation)}>
<DialogContent>
<TextField name="email" label={t`Email`} onChange={() => setIsFormDirty(true)} />
</DialogContent>
<DialogFooter>
<Button type="reset" onPress={handleCancel} variant="secondary" isDisabled={inviteMutation.isPending}> // ✅ Cancel uses handleCancel
<Trans>Cancel</Trans>
</Button>
<Button type="submit" isDisabled={inviteMutation.isPending}> // ✅ Use isDisabled for pending
{inviteMutation.isPending ? <Trans>Sending...</Trans> : <Trans>Send invite</Trans>}
</Button>
</DialogFooter>
</Form>
</>
)}
</Dialog>
</DirtyModal>
);
}
// ❌ DON'T: Common anti-patterns
function BadUserDialog({ users, selectedId, isOpen, onClose }) {
const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values
const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated
const inviteMutation = api.useMutation("post", "/api/users/invite");
useEffect(() => { // ❌ useEffect for calculations - compute inline instead
setFilteredUsers(users.filter(u => u.isActive));
setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types
}, [users, selectedId]);
useEffect(() => { // ❌ useEffect watching isSuccess causes toast timing issues
if (inviteMutation.isSuccess) {
toastQueue.add({ title: "Success", variant: "success" });
}
}, [inviteMutation.isSuccess]);
const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need
return `${user.firstName} ${user.lastName}`;
}, []);
const handleSelect = (id) => console.log(id); // ❌ "handle" + noun (use handleSelectUser), console.log
return (
<Modal isOpen={isOpen} onOpenChange={onClose}> // ❌ Missing isDismissable={!isPending}
<Dialog className="sm:max-w-lg bg-white"> // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background)
{({ close }) => ( // ❌ Both X and Cancel use close (Cancel should use handleCancel)
<>
<XIcon onClick={close} />
<h1>User Mgmt</h1> // ❌ Native <h1> (use Heading), acronym "Mgmt", missing <Trans>
<ul> // ❌ Native <ul> - use ListBox
{filteredUsers.map(user => (
<li key={user.id} onClick={() => handleSelect(user.id)}> // ❌ Native <li>, onClick (use onAction)
<img src={user.avatarUrl} /> // ❌ Native <img> - use Avatar
<Text className="text-sm">{user.email}</Text> // ❌ text-sm with Text causes blur
{getDisplayName(user)}
</li>
))}
</ul>
<Button onPress={close}>Cancel</Button> // ❌ Cancel uses close (shows unwanted warning)
<Button type="submit"> // ❌ Missing isDisabled={isPending}
<Trans>Submit</Trans> // ❌ Missing isPending text pattern, generic "Submit" text
</Button>
</>
)}
</Dialog>
</Modal>
);
}