@noctcore/eslint-plugin-react
React architecture and correctness: prop drilling, state colocation, memoized context values, effect safety.
What it solves
Section titled “What it solves”React components that compile, render, and still rot: a props bundle forwarded unchanged through three layers, state and effects written inline in the .tsx, a context value that is a fresh object every render, a setState that lands after the component unmounted. TypeScript and eslint-plugin-react-hooks see none of these, because each is a structural decision rather than a type or hook-order error.
What it will yell at you about
Section titled “What it will yell at you about”It will yell at you about where code lives as much as what it does. no-state-in-component-body wants state and effects in a colocated <Name>.hooks.ts, max-props-per-component and max-hook-return-surface cap how wide a component or hook is allowed to get, and props-must-be-visual rejects identity props like userId on presentational components. That is a deliberate architecture: containers own data, components render it. It is a bad fit if your codebase keeps hooks inline by convention, or if you are linting a library of generic primitives where wide prop surfaces are the point. Start with the correctness half (context-value-must-be-memoized, require-effect-cancellation, no-effect-derived-state) and switch the structural rules off rather than living with a wall of reports.
Install and configure
Section titled “Install and configure”npm install --save-dev @noctcore/eslint-plugin-react @typescript-eslint/parser# or: bun add -D / pnpm add -D @noctcore/eslint-plugin-react @typescript-eslint/parser// eslint.config.jsimport tsParser from '@typescript-eslint/parser';import react from '@noctcore/eslint-plugin-react';
export default [ { ...react.configs.recommended, files: ['**/*.{ts,tsx}'], languageOptions: { parser: tsParser }, },];The recommended preset enables 14 of 14 rules.
Each rule links to its page, with the options it takes and Incorrect and Correct examples. A good first
read is no-prop-drilling.
| Rule | What it reports | Preset | Fix | Types |
|---|---|---|---|---|
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. | error | autofix | |
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. | error | ||
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. | error | ||
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. | error | ||
max-props-per-component | A *Props interface/type-literal may declare at most max (default 12) local members. extends clauses are not counted. | error | ||
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. | error | ||
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. | error | ||
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. | error | ||
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. | error | ||
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. | error | ||
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. | error | suggestion | |
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. | error | autofix | |
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. | error | ||
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). | error |
Preset: severity in configs.recommended; off means the preset registers the rule switched off, not listed means it leaves the rule out; both are opt-in, so you turn the rule on yourself. Fix: whether the rule ships an autofix or an editor suggestion. Types: whether the rule needs a type-checked program (parserOptions.projectService).