@noctcore/eslint-plugin-react
v0.4.2
Published
React architecture and correctness ESLint rules: prop drilling, state colocation, memoized context, effect safety and guarded web storage.
Maintainers
Readme
@noctcore/eslint-plugin-react
React architecture and correctness rules: unmemoized context values, effects without cleanup, state derived in effects, and components that grow past what one file should hold.
Docs: noctcore.github.io/eslint-plugins/packages/react
Not a good fit for a codebase that keeps hooks inline by convention, or a library of generic primitives where wide prop surfaces are the point.
Requirements
- ESLint 9 or newer, flat config (
eslint.config.js) only. configs.recommendedregisters the plugin and sets rule severities, nothing else. It sets nofilesand no parser, so it applies to whatever files the rest of your config lints. To lint TypeScript, add afilespattern and@typescript-eslint/parser, as in the quick start.
Install
bun add -D @noctcore/eslint-plugin-react @typescript-eslint/parser # or npm i -D / pnpm add -DQuick start
// eslint.config.js
import tsParser from '@typescript-eslint/parser';
import react from '@noctcore/eslint-plugin-react';
export default [
{
...react.configs.recommended,
files: ['**/*.{ts,tsx}'],
languageOptions: { parser: tsParser },
},
// Optional: tune a preset rule's options.
{
files: ['**/*.{ts,tsx}'],
rules: {
'noctcore-react/no-prop-drilling': ['error', { maxForwarded: 4 }],
},
},
];Rules
✅ on in recommended (error) · 🔘 opt-in: not on in recommended, enable it yourself · ⚙️ needs options · 🔧 --fix · 💡 suggestions · 💭 needs type info · ❌ deprecated
| Rule | Description | Preset | ⚙️ | 🔧 | 💡 | 💭 |
| --- | --- | :-: | :-: | :-: | :-: | :-: |
| component-props-naming | A function component's first-param props type must be named <Component>Props. Autofixes by renaming the in-file type declaration and its references when that is safe. | ✅ | | 🔧 | | |
| context-value-must-be-memoized | A context Provider value must not be an inline object literal — a fresh reference each render re-renders every consumer. Memoize it with useMemo and pass the stable reference. | ✅ | | | | |
| max-hook-return-surface | An exported use* hook in a hook file may return object literals (top-level or nested one level) of at most max (default 20) members. Wider returns are god-controllers in the making. | ✅ | | | | |
| max-hooks-per-file | A hook/query/mutation file (default *.hooks.ts / *.queries.ts / *.mutations.ts) may export at most max (default 4) use* hooks. Split larger files. | ✅ | | | | |
| max-props-per-component | A *Props interface/type-literal may declare at most max (default 12) local members. extends clauses are not counted. | ✅ | | | | |
| no-effect-derived-state | An effect whose entire body is setState calls with values derived purely from its dependencies is the "you might not need an effect" anti-pattern — compute the value during render (or with useMemo) instead. | ✅ | | | | |
| no-jsx-computation | Disallow array methods, arithmetic, and chained logical expressions directly inside JSX {...}. Lift them to a const above the return or into the hook. | ✅ | | | | |
| no-jsx-in-hooks | A use*-named function must not return JSX — that is a component wearing a hook costume. Rename it to a PascalCase component, or return values instead of elements. | ✅ | | | | |
| no-prop-drilling | A bundle of maxForwarded+ (default 4) props each forwarded unchanged (name={name}) from the *Props param to the same child component is prop drilling. Compose instead. | ✅ | | | | |
| no-state-in-component-body | State/effect/query hooks must live in the colocated hook file (<Name>.hooks.ts), not in the component .tsx body. The component is a thin shell. | ✅ | | | | |
| no-unguarded-web-storage | A localStorage / sessionStorage call must sit inside a try block: the access itself throws when storage is disabled, partitioned or full. | ✅ | | | 💡 | |
| prefer-lazy-state-init | A useState initializer that is an expensive call (default JSON.parse / localStorage.getItem / sessionStorage.getItem, or a configured builder) must be wrapped in a function so it runs once on mount, not every render. | ✅ | | 🔧 | | |
| props-must-be-visual | Component props must be visual. Auth/business-identity and credential prop names (userId, currentUser, *token*, *jwt*, *secret*, apiKey, ...) are disallowed. A live password input is a legitimate visual concern and is intentionally allowed. | ✅ | | | | |
| require-effect-cancellation | A setState/dispatch that runs after an await or .then() inside a useEffect must be guarded against a unmounted/re-run effect (AbortController, a cancelled flag, or a cleanup return). | ✅ | | | | |
Severity policy
Every rule is error or off, never warn: a warning is a rule nobody obeys. See the
severity policy.
