no-hooks-in-tsx
Pure .tsx files are snapshot-friendly: one render assertion, no hook setup.
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
No hooks in .tsx; logic belongs in .hooks.ts.
Constants, utils, and types each get their own file (.constants.ts,
.utils.ts, .types.ts).
Use cn(...); pull long class strings out for readable conditionals.
PascalCase components; kebab-case otherwise; suffix matches role
(.hooks.ts, .queries.ts, .store.ts).
Required props, optional props, then handlers.
Components read the hook’s view object; no direct TanStack Query, Zustand,
or import.meta.env.
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.
Pure .tsx files are snapshot-friendly: one render assertion, no hook setup.
Hooks return a typed view object; test with renderHook, no DOM.
module-boundaries keeps logic out of .tsx so deadline edits do not collapse into one file.
`.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:
.tsx.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.
scripts/lint-meta/.