Repository navigation
Frontend Structure Guidelines
| Category | Convention | Example |
|---|---|---|
| Page components |
PascalCase + Page suffix |
WasteSearchPage |
| Layout components |
PascalCase, descriptive |
LayoutHeader, LayoutSideNav
|
| Reusable components | PascalCase |
TableResource, ActiveMultiSelect
|
| Domain components |
PascalCase with domain prefix |
WasteSearchFilters, MyClientListing
|
| Hook files |
use prefix, camelCase
|
useBreakpoint, useNotificationEvents
|
| Context files |
PascalCase + Context/Provider
|
AuthContext, AuthProvider
|
| Test files | Source name + .unit.test.tsx or .e2e.test.tsx
|
index.unit.test.tsx |
- Tests: Always co-located next to the source file.
-
Types: Co-located in a
types.tsfile within the same directory. -
Constants: Co-located in a
constants.tsfile within the same directory. -
Utils: Co-located in a
utils.tsfile within the same directory. - Styles: Co-located SCSS modules next to the component (when needed).
- Each custom hook lives in its own directory under
src/hooks/. - Export the hook as the default or named export from
index.tsx. - Hooks that manage subscriptions must clean up in a
useEffectreturn. - Hooks accessing context must validate via null-check and throw if outside provider.
- Notification event hooks must remain React-managed; do not reintroduce a DOM
CustomEventbus for application messaging.
// ✅ Correct: consumer hook with guard
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
};| Context | Scope | When to Use |
|---|---|---|
AuthContext |
Entire app | User identity, token, login/logout |
LayoutContext |
Below Layout
|
Sidenav toggle, header panel state |
ThemeContext |
Below ThemeProvider
|
Carbon theme switching |
PreferenceContext |
Below PreferenceProvider
|
Persisted user preferences |
NotificationContext |
Below NotificationProvider
|
Toast display and notification event delivery |
PageTitleContext |
Below PageTitleProvider
|
Document title management |
Rules:
- Do not use context for server-fetched data — use TanStack Query.
- Do not read context outside its provider boundary.
- Do not store derived state in context — compute it in components.
- Keep context state minimal; complex logic belongs in the provider.
- Route-level:
defaultErrorComponenton the router →GlobalErrorPageinsideLayout. - API-level:
problemDetailsMiddlewarenormalizes toProblemDetails. - Shared hook layer: Emit user-facing query errors from
src/config/react-query/hooks.tsusingnotificationTargetwhen the message belongs to a page-scoped notification flow. - Component-level: Consume scoped notifications with
useScopedNotification; do not translate query errors into notifications inside feature components. - Never swallow errors silently.
- The
routersingleton inrouteTree.tsxis created once at module load — nouseMemoor recreation on auth changes. - Add
React.lazy()when a page grows beyond 50 KB of component code. - Do not nest
<Suspense>boundaries without a clear loading UX.
All useQuery and useInfiniteQuery calls for backend data must go through the shared hooks in
src/config/react-query/hooks.ts. Components must not call useQuery directly.
// ✅ Correct: consume shared hook
import { useCodesQuery } from '@/config/react-query/hooks';
const { data } = useCodesQuery('districtOptions');
// ❌ Incorrect: inline useQuery in component
import { useQuery } from '@tanstack/react-query';
const { data } = useQuery({ queryKey: ['districtOptions'], queryFn: () => API.codes.getDistricts() });Rules:
- Add new hooks to
hooks.tswhen introducing a new query. - All query keys must be defined in
src/config/react-query/queryKeys.tsusing thequeryKeysfactory. - Hook function input types must reuse exported key-factory parameter types (for example
ReportingUnitsQueryParams) instead of redefining local duplicates. - Factory keys follow the shape
[domain, resource, params?]— never use plain string literals. - When notification routing affects cache behavior, include
notificationTargetin the query key factory instead of hiding it in the query function only. - Pass per-callsite options (e.g.
select,enabled,gcTime) as theoptionsargument to the hook, not by re-implementing the hook inline. - Use
notificationTargetin shared hooks when a query failure should surface as a scoped notification banner. - If a shared hook emits notifications from
useEffect, depend on stable primitives such aserrorUpdatedAtand deduplicate with a ref when batching multiple queries. Do not depend on the rawuseQueries()result array.
Use structured array keys for predictable invalidation:
// ✅ Recommended pattern
const queryKey = ['search', 'reporting-units', { filters, page, sort }];
const queryKey = ['codes', 'districts'];
const queryKey = ['forestclient', clientNumber];Rules:
- First element: domain/service name.
- Second element: resource name.
- Third element (optional): parameters object.
- Never use string interpolation in keys.
const mutation = useMutation({
mutationFn: (data) => API.user.updateUserPreferences(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user', 'preferences'] });
notification.display({ kind: 'success', title: 'Saved' });
},
onError: (error) => {
notification.display({ kind: 'error', title: error.title });
},
});- Single global
QueryClient— all queries share the same cache. -
staleTime: 3 hours— data is considered fresh for 3 hours. -
gcTime: 3 hours— garbage collection matches stale time.
- Invalidate after mutations that change server state.
- Use
queryClient.invalidateQueries({ queryKey: ['domain'] })for broad invalidation. - Use exact key matching for targeted invalidation.
- Do not manually set query data unless implementing optimistic updates.
-
retry: false— errors surface immediately. - Use
onErrorcallback for user-visible error handling. - Error shape is always
ProblemDetails(guaranteed by middleware).
| Scenario | Use |
|---|---|
| Read data displayed in UI | useQuery |
| User-triggered write operation | useMutation |
| One-time fetch on mount (no caching) |
useQuery with gcTime: 0
|
| Background data sync |
useQuery with refetchInterval
|
| Fire-and-forget |
mutateAsync without cache update |
- Every backend domain gets its own service class extending
HttpClient. - Services are instantiated once in
APIs.tsand shared via import. - Service methods return
CancelablePromise<T>for cancellation support. - Global middleware belongs in
BackendApiConfig.MIDDLEWARE. - Service methods should not re-register globally configured middleware per request.
- Include
metain request options only when it is defined; avoid emittingmeta: undefinedin strict request-object tests.
// ✅ Correct service method
searchReportingUnit(
filters: ReportingUnitSearchParametersDto,
pageable: PageableRequest<ReportingUnitSearchResultDto>,
meta?: Record<string, unknown>,
): CancelablePromise<PageableResponse<ReportingUnitSearchResultDto>> {
return this.doRequest(this.config, {
method: 'GET',
url: '/api/search/reporting-units',
query: { ...removeEmpty(filters), ...removeEmpty(pageable) },
...(meta !== undefined ? { meta } : {}),
});
}The middleware chain executes in array order:
-
problemDetailsMiddleware— must be first for error normalization. -
failureNotificationMiddleware— emits toast notifications for unscoped failures. -
offlineDataMiddleware— caches responses for offline. -
offlineMutationMiddleware— queues mutations for offline.
Rules:
- Middleware must be stateless.
- Failure handlers must either
throw(to propagate) or return a value (to recover). - Request middleware must always return the config object.
All errors conform to RFC 7807 ProblemDetails:
| Field | Required | Description |
|---|---|---|
type |
No | URI reference (default: about:blank) |
title |
Yes | Human-readable summary |
status |
Yes | HTTP status (0 for network errors) |
detail |
No | Occurrence-specific explanation |
instance |
No | Request URL |
- No automatic retry is configured.
- If retry is needed in the future, implement it as middleware, not at the query level.
- Recommended pattern: exponential backoff with jitter, max 3 retries.
| Entity | Convention | Example |
|---|---|---|
| Types | PascalCase |
FamLoginUser |
| Interfaces |
PascalCase (no I prefix) |
PageableRequest |
| Enums | PascalCase |
Role, RoleType
|
| Enum members | UPPER_SNAKE_CASE |
Role.VIEWER |
| Constants | UPPER_SNAKE_CASE |
AVAILABLE_ROLES |
| Functions | camelCase |
getUserTokenFromCookie |
| Generics | Single uppercase letter or descriptive |
T, PageableResponse<T>
|
-
Use
typefor: union types, mapped types, utility types, function signatures. -
Use
interfacefor: object shapes that may be extended. - Prefer
typeoverinterfacefor DTOs (they are not extended).
- Every service method must be fully typed:
CancelablePromise<ResponseType>. - Use
PageableResponse<T>for paginated endpoints. - Use
ProblemDetailsas the error shape in catch blocks. - Never use
anyfor API responses.
- Shared DTOs and wire schemas live in
src/types/, one*.types.tsmodule per domain, re-exported through theindex.tsbarrel as@/types. The@/typesbarrel is the single stable import surface. -
src/services/types.tsremains as a back-compat re-export barrel until consumers migrate. Do not add new shared types there. - Wire shapes follow the Zod boundary rulebook — see the Frontend Type Boundary pattern.
- Auth types in
src/context/auth/types.ts. - Component prop types co-located in
types.tswithin the component directory; runtime helpers stay component-local (e.g.TableResource/utils.ts).
Use src/env.ts as the only boundary for frontend runtime configuration.
| Category | Rule | Example |
|---|---|---|
| Runtime config keys | Declare every supported window.config string key in the Zod allowlist |
VITE_BACKEND_URL |
| Feature flags | Declare every supported flag in featureFlagsSchema
|
'offline-mode-enabled' |
| Deployment format | Pass flags through VITE_FEATURE_FLAGS on window.config
|
VITE_FEATURE_FLAGS: { 'offline-mode-enabled': false } |
| Consumer access | Read typed booleans from featureFlags, not string truthiness from env
|
featureFlags['offline-mode-enabled'] |
// ✅ Correct
export const featureFlags = {
...getValidatedFeatureFlags(viteFeatureFlags),
...getValidatedFeatureFlags(runtimeFeatureFlags),
};
if (featureFlags['offline-mode-enabled']) {
// enable offline behavior
}// ❌ Incorrect
if (import.meta.env.VITE_FEATURE_FLAGS) {
// serialized flag payloads are strings and are not a typed flag contract
}Rules:
- Reject unknown runtime config keys instead of silently accepting them.
- Do not add ad hoc
window.configkeys outsideenv.ts. - Do not store deployment feature flags in
localStorage. - When adding a new flag, update the schema, the deployment config, and tests in the same change.
- Use
describe/itblocks with descriptive names. - Use
@testing-library/reactfor component rendering. - Use
@testing-library/user-eventfor interaction simulation. - Mock external dependencies in
setup-env.ts, not per-test. - Use
vi.fn()for function mocks,vi.spyOn()for partial mocks. - Prefer
screen.getByRole()overgetByTestId().
- Use Page Object pattern for complex pages.
- Mock APIs via
mockApi()/mockApiResponses()frome2e.helper.ts. - Use auth fixtures from
auth.helper.tsfor authenticated flows. - Tag tests:
*.e2e.test.tsxfor E2E,*.a11y.test.tsxfor accessibility.
| Dependency | Mock Location | Approach |
|---|---|---|
| AWS Amplify | setup-env.ts |
Global vi.mock |
| APIs service | setup-env.ts |
Global vi.mock |
| Browser APIs (matchMedia, ResizeObserver) | setup-env.ts |
Polyfill stubs |
| Backend routes (E2E) | Per-test |
page.route() via helpers |
| Layer | What to Test | Framework |
|---|---|---|
| Utils, pure functions | Input/output, edge cases | Vitest |
| Custom hooks | State changes, side effects | Vitest + renderHook |
| Components | Rendering, interaction, accessibility | Vitest + Testing Library |
| Pages (integration) | Navigation, API integration, flow | Playwright |
| Accessibility | WCAG compliance, contrast, labels | Playwright + Axe |
- Tokens are stored in cookies (Cognito
CookieStorage), notlocalStorage. - Tokens are retrieved from cookies per-request via
getUserTokenFromCookie()through an async resolver. -
BackendApiConfig.TOKENis initialized toundefined; the async resolver is the sole setter, preventing a stale synchronous token snapshot at startup. - Token refresh occurs every 3 minutes in
AuthProvider. - Never log tokens or include them in error reports.
- Carbon components handle XSS for rendered text.
- Do not use
dangerouslySetInnerHTML. - Sanitize user input before sending to APIs.
- Always use
removeEmpty()to strip falsy values from query parameters. - Use the
queryoption (not string concatenation) for query parameters. - Use path templates (
{param}) withencodeURIfor URL path segments. - Never interpolate user input directly into URLs.
- Treat
window.configas untrusted input and validate it before use. - Keep runtime config parsing centralized in
src/env.ts. - Fail fast on malformed
VITE_FEATURE_FLAGSJSON or unexpected flag names. - Prefer typed feature flag consumers over string comparisons in components and hooks.
- Service workers run on a separate thread; ensure they do not expose tokens.
- IndexedDB data should be scoped to the authenticated user.
- Queued mutations must include auth context for replay.
- Use HTTPS only (enforced by Caddy/OpenShift).
Manual chunks in vite.config.ts split vendor code:
| Chunk | Libraries |
|---|---|
react-vendor |
react, react-dom |
carbon-charts |
@carbon/charts |
carbon |
@carbon/* |
aws |
aws-amplify |
query |
@tanstack/* |
router |
@tanstack/react-router |
utils |
axios, lodash, luxon, idb |
- Use
useMemofor expensive computations that depend on stable inputs. - Use
useCallbackfor functions passed as props to memoized children. - Do not memoize everything — only optimize measured bottlenecks.
- The
routersingleton inrouteTree.tsxis a module-level constant and does not need memoization.
-
optimizeDeps.includepre-bundles heavy dependencies for faster cold starts. -
chunkSizeWarningLimit: 1024flags chunks over 1 MB. -
server.hmr.overlay: falseavoids intrusive error overlays during development. - Font and image preloading in
index.htmlreduces first-paint latency.
- Use Carbon components as the base — they include ARIA attributes by default.
- Every interactive element must have an accessible name (
aria-labelor visible label). - Every page must have a single
<h1>(provided byPageTitle). - Use semantic HTML:
<nav>,<main>,<header>,<footer>. - Color must not be the sole means of conveying information.
- Focus management must be handled on route transitions.
- Run
test:a11y(Playwright + Axe) on all pages. - Axe violations at
criticalandseriousimpact levels must be fixed. - Add
*.a11y.test.tsxfiles for each page. - Test keyboard navigation for all interactive flows.
- Verify screen reader output for key user journeys.
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository