@boring-stack-pkg/eslint-plugin-react-component-architecture
v0.3.0
Published
ESLint plugin enforcing React component architecture conventions.
Maintainers
Readme
eslint-plugin-react-component-architecture
ESLint plugin enforcing React component architecture conventions from AGENTS.md.
Why
React components benefit from enforcing a strict folder structure and separation of concerns. This plugin ensures that:
- Components are properly structured with hooks, types, tests, and stories as siblings
- State management is isolated in custom hooks, not in component bodies
- JSX templates stay minimal and clean (no inline functions or complex logic)
- Props describe visual state only, not business logic
- TypeScript interfaces follow naming conventions
- Imports use consistent patterns (e.g., named imports for React)
- Dark mode classes are removed (light mode only)
Install
pnpm add -D @boring-stack-pkg/eslint-plugin-react-component-architecture @typescript-eslint/parserUsage (flat config)
// eslint.config.mjs
import tsParser from "@typescript-eslint/parser";
import reactComponentArchitecture from "@boring-stack-pkg/eslint-plugin-react-component-architecture";
export default [
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaVersion: "latest", sourceType: "module", ecmaFeatures: { jsx: true } },
},
plugins: { "react-component-architecture": reactComponentArchitecture },
rules: reactComponentArchitecture.configs.recommended.rules,
},
];The recommended preset enables all 15 rules. Most rules are set to "error". Heuristic rules (no-state-in-component-body, no-jsx-computation, props-must-be-visual) default to "warn".
Rules
| Rule | Type | Description |
|------|------|-------------|
| component-folder-structure | Problem | Enforce required sibling files in component folders |
| index-must-reexport-default | Problem | index.ts must re-export component default export |
| no-state-in-component-body | Suggestion | Move state hooks to custom hooks (.hooks.ts) |
| no-inline-jsx-functions | Suggestion | Use function references instead of inline functions for handlers |
| no-jsx-computation | Suggestion | Extract complex JSX computations into hooks |
| classnames-required | Suggestion | Use classNames for dynamic className values |
| classnames-import-name | Suggestion | Import classnames with correct name |
| no-dark-mode-classes | Suggestion | Remove dark: Tailwind classes (light mode only) |
| interface-prefix-i | Suggestion | Interfaces should be prefixed with 'I' |
| forwardref-display-name | Problem | forwardRef components must have displayName |
| stories-require-default-export | Problem | Story files must export Default |
| props-must-be-visual | Suggestion | Props should be visual, not business logic |
| react-import-named | Suggestion | Use named imports from React |
| package-json-exact-deps | Problem | Enforce exact dependency versions |
| github-actions-permissions | Problem | Enforce permissions and pinned action refs |
Examples
no-state-in-component-body
// ❌ State in component body
function Counter() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
// ✅ State in custom hook
function useCounter() {
const [count, setCount] = useState(0);
return { count, setCount };
}
function Counter() {
const { count } = useCounter();
return <div>{count}</div>;
}no-inline-jsx-functions
// ❌ Inline function
function Button() {
return <button onClick={() => alert('clicked')}>Click</button>;
}
// ✅ Named function reference
function Button() {
const handleClick = () => alert('clicked');
return <button onClick={handleClick}>Click</button>;
}classnames-required
// ❌ Ternary in className
<button className={isActive ? 'active' : 'inactive'}>Click</button>
// ✅ Use classNames utility
import classNames from 'classnames';
<button className={classNames({ active: isActive })}>Click</button>Component Folder Structure
Each component should follow this structure:
ComponentName/
├── ComponentName.tsx // Main component
├── ComponentName.types.ts // TypeScript interfaces
├── ComponentName.hooks.ts // Custom hooks (if needed)
├── ComponentName.stories.tsx // Storybook stories
├── ComponentName.test.ts // Tests
├── ComponentName.utils.ts // Utilities (optional)
├── ComponentName.constants.ts // Constants (optional)
└── index.ts // ExportsThe index.ts must contain:
export { default as ComponentName } from "./ComponentName";
export * from "./ComponentName.types";Notes
- Rules 14 & 15:
package-json-exact-depsandgithub-actions-permissionsperform file-content parsing and are best used with appropriate parsers configured in your ESLint setup. - All rules support configuration options. See individual rule docs for details.
