Skip to content
BoringStack
GitHub

Architecture rules

3 min read

UI Lint Contract

The apps/ui enforces its architecture through ESLint, not vibes. The UI-specific work is @boring-stack-pkg/eslint-plugin-react-component-architecture. It composes with the shared plugins (resource-architecture, module-boundaries, structured-logging, env-access, test-conventions, code-flow) that both apps use.

ESLint

enforcement layer

7

rule categories

Compile-time

not vibes

Component anatomy

No hooks in .tsx; logic belongs in .hooks.ts.

Single semantic module

Constants, utils, and types each get their own file (.constants.ts, .utils.ts, .types.ts).

className discipline

Use cn(...); pull long class strings out for readable conditionals.

File naming

PascalCase components; kebab-case otherwise; suffix matches role (.hooks.ts, .queries.ts, .store.ts).

Prop ordering

Required props, optional props, then handlers.

View object boundary

Components read the hook’s view object; no direct TanStack Query, Zustand, or import.meta.env.

Queries layer

useQuery only in *.queries.ts; component hooks call those query hooks.

Each rule has a fix-it suggestion where mechanical; the rest fail the lint gate and need a real edit.

01

no-hooks-in-tsx

Pure .tsx files are snapshot-friendly: one render assertion, no hook setup.

02

view object in .hooks.ts

Hooks return a typed view object; test with renderHook, no DOM.

03

no-mixed-concern-modules

module-boundaries keeps logic out of .tsx so deadline edits do not collapse into one file.

04

consistent file suffixes

`.hooks.ts`, `.queries.ts`, `.store.ts`: same layout in every feature folder.

// eslint-disable-next-line <rule> works, but every suppression is reviewed. Common valid cases:

  • Third-party render-prop APIs that force a hook-like pattern in .tsx.
  • Sub-components that have no state but are too small to split into their own folder. Inline them; the lint rule has a size threshold.

If you find yourself suppressing the same rule across many files, that’s a signal to update the rule, not to keep suppressing.

Plugin source + per-rule docs: @boring-stack-pkg/eslint-plugin-react-component-architecture. apps/ui ESLint config: eslint.config.mjs.