This document provides guidance specific to working on the ui module. For
contribution guidelines for the Pathling core libraries, see the main
CONTRIBUTING.md in the repository root.
You will need the following software:
Install dependencies and start the development server:
cd ui
bun install
bun run devThe development server runs at http://localhost:5173/admin/.
- React 19 with TypeScript
- Vite for development and building
- Radix UI Themes for accessible UI components
- TanStack Query for server state management
- React Router for client-side routing
- fhirclient for SMART on FHIR authentication
Build the production bundle:
bun run buildThe output is written to dist/.
Unit tests use Vitest with
Testing Library.
Test files are located in __tests__ directories alongside their source files
(e.g., src/api/__tests__/rest.test.ts).
# Watch mode
bun run test
# Single run
bun run test:run
# With coverage
bun run test:coverageCoverage thresholds are set to 80% for lines, functions, branches, and statements.
E2E tests use Playwright. Test files are in the
e2e/ directory.
# Headless
bun run test:e2e
# With UI
bun run test:e2e:ui
# Headed browser
bun run test:e2e:headed- Follow the Arrange-Act-Assert pattern.
- Mock external APIs using
vi.fn()andvi.stubGlobal(). - Test both success and error cases (401, 404, 500 responses).
- Reset mocks in
afterEachblocks. - Put significant logic into plain functions and unit test those comprehensively. Keep hooks as thin wrappers that compose plain functions with React primitives.
- Use role-based selectors (
getByRole,getByText) in E2E tests. - Mock network requests using
page.route()in Playwright tests.
- No classes. Avoid the
classkeyword entirely. Use plain functions, closures, and immutable data structures. The only exception is error boundaries, which require class components. - Pure functional style. Prefer composition over inheritance. Use
typeandinterfacefor data shapes, and express behaviour through standalone functions. - Use lower camel case for file names. Name files using
lowerCamelCase.ts(e.g.,bulkExport.ts,useCreate.ts). The exception is React component files, which use Pascal case (e.g.,ErrorBoundary.tsx). - Comprehensive JSDoc on all exported functions. Every exported function
must have a JSDoc comment that includes a description,
@paramtags,@returnstag, and@throwstags where applicable.
- Functional components only. Class components are only permitted for error boundaries.
- Components must be pure. Same props and state must produce the same output.
- Never nest component definitions inside other components.
- Extract significant logic into custom hooks. Components should primarily compose hooks and render UI.
- Local state first. Use
useState/useReducerbefore reaching for context. Only use React Context for state that genuinely needs to be shared across distant components (auth, jobs, toasts). - TanStack Query for all server state. Use TanStack Query for data fetching, caching, and mutations.
- Avoid effects where possible. Do not use effects to transform data for rendering or to handle user events. Effects are escape hatches, not primary tools.
| Element | Convention | Example |
|---|---|---|
| Components | PascalCase |
ExportForm |
| Props and variables | camelCase |
resourceType |
| Event handler props | on prefix |
onClick, onSubmit |
| Event handler methods | handle |
handleClick, handleSubmit |
| Custom hooks | use prefix |
useAuth, useBulkExport |
| Boolean props | omit ={true} |
<Foo hidden /> |
| Files | camelCase |
bulkExport.ts, useCreate.ts |
| Component files | PascalCase |
ExportForm.tsx |
Imports are enforced by ESLint and must follow this order, with blank lines between groups:
- Built-in and external packages
- Internal modules
- Parent, sibling, and index imports
- Type imports
- Use semantic HTML elements (
<button>,<a>, landmarks, proper headings,<label>withhtmlFor). - All interactive elements must be keyboard accessible.
- Use ARIA attributes (
aria-label,aria-labelledby,aria-describedby) where visible labels are not present. - Trap focus inside modals and dialogs.
Follow this hierarchy when choosing where to put state:
- Local state (
useState/useReducer) for single components. - Lifted state when two or three siblings need the same data.
- Context API for truly global, infrequently-changing data (themes, auth, locale).
- TanStack Query for all server state (API data, caching, refetching).
Measure before optimising. Use React DevTools Profiler before reaching for
useMemo, useCallback, or React.memo.
ESLint 9 with flat config format. The configuration is in eslint.config.js
and includes type-aware TypeScript linting, React hooks rules, accessibility
checks, JSDoc enforcement, and import ordering.
bun run lint
bun run lint:fixPrettier with default settings.
bun run format
bun run format:checkjscpd detects copy-paste duplication.
bun run lint:duplicationTypeScript strict mode is enabled with noUnusedLocals,
noUnusedParameters, and noFallthroughCasesInSwitch.
Run the following after making code changes to ensure quality:
bun run buildbun run lintbun run lint:duplicationbun run test:coveragebun run test:e2e