Rules reference
413 engine rules grouped by category. Each rule pairs a validation prompt (when to confirm vs. suppress a finding) with a fix prompt (how to address it).
Every rule is also fetchable as Markdown at /docs/rules/{plugin}/{rule}.md, for example effect/no-derived-state.
Accessibility
jsx-a11y/alt-textjsx-a11y/anchor-is-validjsx-a11y/click-events-have-key-eventsjsx-a11y/heading-has-contentjsx-a11y/html-has-langjsx-a11y/iframe-has-titlejsx-a11y/label-has-associated-controljsx-a11y/no-autofocusjsx-a11y/no-distracting-elementsjsx-a11y/no-redundant-rolesjsx-a11y/no-static-element-interactionsjsx-a11y/role-has-required-aria-propsjsx-a11y/scopejsx-a11y/tabindex-no-positivereact-doctor/anchor-ambiguous-text: Describe a link's destination — avoid bare 'click here' / 'learn more' / 'link' as the only link text.react-doctor/anchor-has-content: Add visible or aria-labelled text inside every `<a>`.react-doctor/aria-activedescendant-has-tabindex: Add `tabIndex` to elements with `aria-activedescendant` so they're keyboard-focusable.react-doctor/aria-props: Use only documented aria-* attributes from the WAI-ARIA spec.react-doctor/aria-proptypes: Give each aria-* attribute a value matching its WAI-ARIA type (boolean, token, integer, ID, ID-list, etc.).react-doctor/aria-role: Use a documented, non-abstract WAI-ARIA role for every role attribute.react-doctor/aria-unsupported-elements: Don't put role / aria-* attributes on reserved HTML elements like meta, head, script, or style.react-doctor/autocomplete-valid: Use a valid HTML autofill token in autoComplete.react-doctor/control-has-associated-label: Give every interactive control an accessible label via visible text, aria-label, aria-labelledby, or an associated label.react-doctor/design-no-vague-button-label: Name the action: "Save changes" instead of "Continue", "Send invite" instead of "Submit", "Delete account" instead of "OK". The label IS the button's accessible namereact-doctor/img-redundant-alt: Drop redundant words like 'image' / 'photo' / 'picture' from alt text and describe the content instead.react-doctor/interactive-supports-focus: Add tabIndex to elements that have interactive roles and event handlers.react-doctor/lang: Use a valid BCP-47 language tag on `<html lang>` (e.g. `en` / `en-US`).react-doctor/media-has-caption: Add a `<track kind="captions">` child to every `<audio>` / `<video>`.react-doctor/mouse-events-have-key-events: Pair mouse events with their keyboard equivalents.react-doctor/no-access-key: Don't use `accessKey` — it conflicts with assistive-technology shortcuts.react-doctor/no-aria-hidden-on-focusable: Remove `aria-hidden` from focusable elements (or remove the focusability).react-doctor/no-disabled-zoom: Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If your layout breaks at 200% zoom, fix the layout — don't punish users with disabilitiesreact-doctor/no-gray-on-colored-background: Use a darker shade of the background color for text, or white/near-white for contrast. Gray text on colored backgrounds looks washed outreact-doctor/no-interactive-element-to-noninteractive-role: Don't override an interactive element's semantics with a non-interactive role.react-doctor/no-justified-text: Use `text-align: left` for body text, or add `hyphens: auto` and `overflow-wrap: break-word` if you must justifyreact-doctor/no-noninteractive-element-interactions: Move the interaction to a semantic interactive element, or add an interactive role plus keyboard support.react-doctor/no-noninteractive-element-to-interactive-role: Use a semantic interactive element instead of role-promoting a non-interactive one.react-doctor/no-noninteractive-tabindex: Reserve tabIndex for interactive elements or interactive roles; remove it from non-interactive ones.react-doctor/no-outline-none: Use `:focus-visible { outline: 2px solid var(--color-muted); outline-offset: 2px }` to show focus only for keyboard users while hiding it for mouse clicksreact-doctor/no-tiny-text: Use at least 12px for body content, 16px is ideal. Small text is hard to read, especially on high-DPI mobile screensreact-doctor/prefer-html-dialog: Replace the hand-rolled modal wrapper with a native <dialog> opened via dialog.showModal()react-doctor/prefer-tag-over-role: Replace role with the semantic HTML element when one exists.react-doctor/require-reduced-motion: Project ships a motion library but never gates animation on the user's reduced-motion preference — add `useReducedMotion()` / `<MotionConfig reducedMotion="user">` or a `@media (prefers-reduced-motion: reduce)` queryreact-doctor/role-supports-aria-props: Use only aria-* props that are supported by the element's explicit or implicit ARIA role.
Architecture
deslop/complex-function: Flag functions exceeding configured cyclomatic/cognitive/param/line thresholds; suggest decomposing.deslop/cross-file-duplicate-export: Flag the same export name emitted from multiple files that share a common importer (ambiguous public name).deslop/duplicate-block: Disallow copy-pasted code blocks duplicated across locations.deslop/duplicate-constant: Unify the same literal value duplicated as constants across 3+ files into one shared constant.deslop/duplicate-export: Flag a name exported more than once from a single file, e.g. `export { x } from "./a"` plus a later `export const x`, collapsing to one canonical export.deslop/duplicate-import: Merge multiple import statements from the same module specifier into one.deslop/duplicate-inline-type: Disallow repeating the same inline object type literal; extract a named type or interface.deslop/duplicate-type-definition: Flag named type aliases/interfaces with the same structural shape duplicated across files; extract one shared type.deslop/identity-wrapper: Flag a thin function that just forwards its args to another callable unchanged, e.g. const getUser = (id) => fetchUser(id).deslop/private-type-leak: Disallow exporting a symbol whose signature references a locally-declared, non-exported type.deslop/redundant-alias: Disallow an alias that renames a symbol to itself or round-trips with no net change, e.g. `import { X as X }`.deslop/redundant-type-pattern: Disallow redundant type expressions with a no-op operand, e.g. `T & {}`, `Partial<Partial<T>>`, `Pick<X, keyof X>`.deslop/shadowed-directory: Flag two directories that look like a forked/copied tree (e.g. src/ vs deno/lib/) sharing many duplicated same-named files.react-doctor/design-no-bold-heading: Use `font-semibold` (600) or `font-medium` (500) on headings — 700+ crushes letter counter shapes at display sizesreact-doctor/design-no-default-tailwind-palette: Replace `indigo-*` / `gray-*` / `slate-*` with project tokens, your brand color, or a less-default neutral (`zinc`, `neutral`, `stone`)react-doctor/design-no-em-dash-in-jsx-text: Replace em dashes in JSX prose with commas, colons, semicolons, or parentheses so UI copy reads less like generated text.react-doctor/design-no-redundant-padding-axes: Collapse `px-N py-N` to `p-N` when both axes match. Keep them split only when one axis varies at a breakpoint (`py-2 md:py-3`)react-doctor/design-no-redundant-size-axes: Collapse `w-N h-N` to `size-N` (Tailwind v3.4+) when both axes matchreact-doctor/design-no-space-on-flex-children: Use `gap-*` on the flex/grid parent. `space-x-*` / `space-y-*` produce phantom gaps when a sibling is conditionally rendered, lose vertical spacing on wrapped lines, and don't mirror in RTLreact-doctor/design-no-three-period-ellipsis: Use the typographic ellipsis "…" (or `…`) instead of three periods — pairs with action-with-followup labels ("Rename…", "Loading…")react-doctor/display-name: Give each component a stable displayName so React DevTools shows a real name instead of "Unknown".react-doctor/forbid-component-props: Configure forbidden props per component via the `forbidComponentProps.forbid` setting.react-doctor/forbid-dom-props: Configure forbidden DOM props via the `forbidDomProps.forbid` setting to keep disallowed attributes off DOM nodes.react-doctor/forbid-elements: Replace each configured forbidden element with its sanctioned component or wrapper.react-doctor/forward-ref-uses-ref: Either accept a `ref` parameter in the forwardRef render function, or drop the forwardRef wrapper entirely.react-doctor/hook-use-state: Destructure useState as `const [thing, setThing] = useState(…)`.react-doctor/jsx-boolean-value: Pick one boolean-attribute style codebase-wide (default: omit `={true}`, e.g. write `<C foo />`).react-doctor/jsx-curly-brace-presence: Pick a consistent quoting style for JSX literal values and drop redundant curly braces around plain strings.react-doctor/jsx-filename-extension: Use .jsx / .tsx (or your project's chosen extension) for files containing JSX.react-doctor/jsx-fragments: Pick one fragment style across the codebase — use the <></> shorthand by default.react-doctor/jsx-handler-names: Use the `on…` prefix for event-handler props and `handle…` for the functions that handle them.react-doctor/jsx-max-depth: Extract deeply nested JSX into smaller components to keep render trees readable.react-doctor/jsx-no-useless-fragment: Drop the fragment when it wraps a single child or holds multiple children directly under an HTML tag.react-doctor/jsx-pascal-case: Rename custom JSX components to PascalCase.react-doctor/jsx-props-no-spreading: List each prop explicitly so consumers can see what's being passed instead of spreading.react-doctor/no-clone-element: Pass children, render props, or Children.map instead of cloning elements with React.cloneElement.react-doctor/no-dark-mode-glow: Use a subtle `box-shadow` with neutral colors for depth, or `border` with low opacity. Colored glows on dark backgrounds are the default AI-generated aestheticreact-doctor/no-default-props: React 19 removes `Component.defaultProps` for function components. Move the defaults into the destructured props parameter: `function Foo({ size = "md", variant = "primary" })` instead of `Foo.defaultProps = { size: "md", variant: "primary" }`.react-doctor/no-generic-handler-names: Rename to describe the action: e.g. `handleSubmit` → `saveUserProfile`, `handleClick` → `toggleSidebar`react-doctor/no-giant-component: Extract logical sections into focused components: `<UserHeader />`, `<UserActions />`, etc.react-doctor/no-gradient-text: Use solid text colors for readability. If you need emphasis, use font weight, size, or a distinct color instead of gradientsreact-doctor/no-inline-exhaustive-style: Move styles to a CSS class, CSS module, Tailwind utilities, or a styled component — inline objects with many properties hurt readability and create new references every renderreact-doctor/no-many-boolean-props: Split into compound components or named variants: `<Button.Primary />`, `<DialogConfirm />` instead of stacking `isPrimary`, `isConfirm` flagsreact-doctor/no-multi-comp: Move secondary components into their own files.react-doctor/no-polymorphic-children: Expose explicit subcomponents (`<Button.Text>`, `<Button.Icon>`) so consumers don't need to switch on `typeof children`react-doctor/no-prop-types: Move propTypes to TypeScript types: `type Props = { value: number }; function Component(props: Props)` — React 19 ignores runtime propTypesreact-doctor/no-pure-black-background: Tint the background slightly toward your brand hue — e.g. `#0a0a0f` or Tailwind's `bg-gray-950`. Pure black looks harsh on modern displaysreact-doctor/no-react-children: Pass children as props or render them directly instead of calling React.Children methods.react-doctor/no-react-dom-deprecated-apis: Switch the legacy `react-dom` root API (`render` / `hydrate` / `unmountComponentAtNode`) to `createRoot` / `hydrateRoot` / `root.unmount()` from `react-dom/client`. Replace `findDOMNode` with a ref. The whole `react-dom/test-utils` entry point is removed in React 19 — use `act` from `react` and `fireEvent` / `render` from `@testing-library/react`. Only enabled on projects detected as React 18+.react-doctor/no-react19-deprecated-apis: Pass `ref` as a regular prop on function components — `forwardRef` is no longer needed in React 19+. Replace `useContext(X)` with `use(X)` for branch-aware context reads. Only enabled on projects detected as React 19+.react-doctor/no-redundant-should-component-update: Drop shouldComponentUpdate when extending PureComponent, or extend React.Component if custom comparison logic is genuinely needed.react-doctor/no-render-in-render: Extract to a named component: `const ListItem = ({ item }) => <div>{item.name}</div>`react-doctor/no-render-prop-children: Replace `renderXxx` props with compound subcomponents (e.g. `<Modal.Header>`) or `children` so the parent doesn't dictate every customization pointreact-doctor/no-set-state: Lift state up or use an external store instead of this.setState.react-doctor/no-side-tab-border: Use a subtler accent (box-shadow inset, background gradient, or border-bottom) instead of a thick one-sided borderreact-doctor/no-unescaped-entities: Replace bare ' / " / > / } characters in JSX text with HTML entities.react-doctor/no-wide-letter-spacing: Reserve wide tracking (letter-spacing > 0.05em) for short uppercase labels, navigation items, and buttons — not body textreact-doctor/no-z-index-9999: Define a z-index scale in your design tokens (e.g. dropdown: 10, modal: 20, toast: 30). Create a new stacking context with `isolation: isolate` instead of escalating valuesreact-doctor/only-export-components: Move non-component exports out of files that export components.react-doctor/prefer-es6-class: Use one component style consistently — ES2015 `class extends React.Component` (default) over the legacy `createReactClass` factory.react-doctor/prefer-function-component: Re-write the class component as a function component using hooks.react-doctor/prefer-module-scope-pure-function: Hoist the pure helper to module scope (above the component) so it isn't reallocated each render: const formatName = (user) => ...react-doctor/prefer-module-scope-static-value: Hoist the static array/object literal to module scope above the component: const FILTER_OPTIONS = ["all", "active", "done"]; function App() { ... }react-doctor/react-compiler-destructure-method: Destructure the method up front: `const { push } = useRouter()` then call `push(...)` directly — clearer dependency graph and easier for React Compiler to memoizereact-doctor/react-compiler-no-manual-memoization: Delete the React useMemo / useCallback / memo wrapper — React Compiler memoizes it for you.react-doctor/self-closing-comp: Use the self-closing form `<X />` for elements with no children.react-doctor/state-in-constructor: Pick one state-initialization style for class components — class field or constructor — and use it consistently.react-doctor/zod-v4-no-deprecated-error-apis: Replace deprecated ZodError helpers with Zod 4 functions: z.treeifyError(), z.flattenError(), z.prettifyError(), or read error.issues directlyreact-doctor/zod-v4-no-deprecated-error-customization: Replace deprecated Zod error customization with the v4 unified { error } API: z.string({ error: "Required" })react-doctor/zod-v4-no-deprecated-schema-apis: Migrate deprecated Zod 4 schema APIs: z.object().strict() to z.strictObject(), z.nativeEnum to z.enum, z.record(value) to z.record(key, value), z.function().args().returns() to z.function({ input, output }).react-doctor/zod-v4-prefer-top-level-string-formats: Replace z.string().<format>() with the Zod 4 top-level format API, e.g. z.email() or z.uuid()
Bundle Size
react-doctor/no-barrel-import: Import from the direct path: `import { Button } from './components/Button'` instead of `./components`react-doctor/no-dynamic-import-path: Use a string-literal path: `import('./feature/heavy.js')` so the bundler can split this chunkreact-doctor/no-full-lodash-import: Import the specific function: `import debounce from 'lodash/debounce'` — saves ~70kbreact-doctor/no-moment: Replace with `import { format } from 'date-fns'` (tree-shakeable) or `import dayjs from 'dayjs'` (2kb)react-doctor/no-undeferred-third-party: Use `next/script` with `strategy="lazyOnload"` or add the `defer` attributereact-doctor/prefer-dynamic-import: Use `const Component = dynamic(() => import('library'), { ssr: false })` from next/dynamic or React.lazy()react-doctor/use-lazy-motion: Use `import { LazyMotion, m } from "framer-motion"` with `domAnimation` features — saves ~30kb
Correctness
deslop/commonjs-in-esm: Flag CommonJS constructs (require/module.exports/exports.x) inside an ESM module.deslop/lazy-import-at-top-level: Flag a dynamic import() at module top level that is awaited or .then/.catch/.finally-ed during load (no laziness benefit); prefer a static import.deslop/simplifiable-expression: Disallow expressions that collapse to a simpler equivalent, e.g. !!x → Boolean(x).deslop/simplifiable-function: Disallow functions written less directly than needed: block-arrow-single-return, redundant-await-return, useless-async-no-await.deslop/ts-escape-hatch: Disallow TypeScript suppressions that hide type errors.deslop/unnecessary-assertion: Flag TypeScript assertions that are no-ops or weaken types (`x!!`, `as any`, `as unknown as T`, `<T>x`).react-doctor/button-has-type: Set type="button" (or "submit"/"reset") explicitly on every <button> so it never defaults to submit.react-doctor/checked-requires-onchange-or-readonly: Pair `checked` with `onChange={…}` (controlled) or `readOnly` (display-only), and never combine `checked` with `defaultChecked`.react-doctor/client-localstorage-no-version: Bake a version into the storage key (e.g. "myKey:v1"); a future schema change can ignore old data instead of crashing on itreact-doctor/exhaustive-deps: Match the deps array to what the hook callback actually captures, or stabilize/move recreated values instead of blindly adding them.react-doctor/html-no-invalid-paragraph-child: Replace the wrapping <p> with a <div>, or hoist the block-level child out of the paragraphreact-doctor/html-no-invalid-table-nesting: Author each table element under its required host parent: thead/tbody/tfoot in <table>, tr in a row group, td/th in <tr>react-doctor/html-no-nested-interactive: Hoist the inner interactive element out, or make the outer one a non-interactive wrapper: change <a><a/></a> to <a/> next to <a/>, or wrap with <div>/<span>react-doctor/jsx-no-comment-textnodes: Wrap JSX comments in `{/* … */}` so they're parsed as comments, not rendered as literal child text.react-doctor/jsx-no-undef: Import the component or fix the typo so the JSX element name resolves to a real binding.react-doctor/jsx-props-no-spread-multi: Spread each unique expression at most once per JSX element.react-doctor/no-array-index-as-key: Use a stable unique identifier: `key={item.id}` or `key={item.slug}` — index keys break on reorder/filterreact-doctor/no-create-context-in-render: Move createContext to module scope so its Context identity stays stable across rendersreact-doctor/no-create-store-in-render: Hoist the store/atom/observable construction to module scope: const store = create(...) outside the component.react-doctor/no-danger-with-children: Use either children or dangerouslySetInnerHTML on an element, never both.react-doctor/no-document-start-view-transition: Render a <ViewTransition> component and update inside startTransition / useDeferredValue — React calls startViewTransition for youreact-doctor/no-find-dom-node: Use refs (useRef/createRef) to access DOM nodes instead of the removed findDOMNode API.react-doctor/no-legacy-class-lifecycles: Move side effects in `componentWillMount` to `componentDidMount`; replace `componentWillReceiveProps` with `componentDidUpdate` (compare prevProps) or the static `getDerivedStateFromProps` for pure state derivation; replace `componentWillUpdate` with `getSnapshotBeforeUpdate` paired with `componentDidUpdate`. The `UNSAFE_` prefix only silences the warning — React 19 removes both forms.react-doctor/no-legacy-context-api: Replace `childContextTypes` + `getChildContext` with `const MyContext = createContext(...)` + `<MyContext.Provider value={...}>`; replace `contextTypes` with `static contextType = MyContext` (single context) or `useContext()` / `use()` from a function component. The provider and every consumer must migrate together — partial migrations leave consumers reading the wrong context.react-doctor/no-namespace: Drop the namespace and use a plain (Pascal-cased) component or DOM tag.react-doctor/no-nested-component-definition: Move to a separate file or to module scope above the parent componentreact-doctor/no-prevent-default: Use `<form action={serverAction}>` (works without JS) or `<button>` instead of `<a>` with preventDefaultreact-doctor/no-random-key: Replace the fresh-each-render key with a stable id from the item: key={item.id}react-doctor/no-this-in-sfc: Use the function's `props` parameter instead of `this.props` in stateless function components.react-doctor/no-uncontrolled-input: Pass an explicit initial value to `useState` (e.g. `useState("")` instead of `useState()`), add `onChange` (or `readOnly` to opt out) when you supply `value`, and drop `defaultValue` on controlled inputs — React ignores itreact-doctor/no-unsafe: Replace UNSAFE_componentWillMount / WillReceiveProps / WillUpdate with their modern lifecycle equivalents.react-doctor/react-in-jsx-scope: If on React 17+ with the new JSX transform, disable this rule; otherwise import React at the top of the file.react-doctor/rendering-conditional-render: Change to `{items.length > 0 && <List />}` or use a ternary: `{items.length ? <List /> : null}`react-doctor/rendering-hydration-mismatch-time: Wrap dynamic time/random values in useEffect+useState (client-only) or add suppressHydrationWarning to the parent if intentionalreact-doctor/style-prop-object: Pass the `style` prop as an object literal like `{{ color: 'red' }}`, never a string or other primitive.react-doctor/void-dom-elements-no-children: Remove children from the void element, or use a non-void element if children are needed.react/jsx-keyreact/jsx-no-duplicate-propsreact/jsx-no-script-urlreact/no-children-propreact/no-dangerreact/no-direct-mutation-statereact/no-is-mountedreact/no-render-return-valuereact/no-string-refsreact/no-unknown-propertyreact/require-render-returnreact/rules-of-hooks
Dead Code
deslop/circular-dependency: Disallow runtime import cycles between modules (A imports B ... imports A).deslop/feature-flag: Disallow stale feature flags that permanently guard dead code.deslop/misclassified-dependency: Move a runtime dep consumed only via `import type` into devDependencies.deslop/re-export-cycle: Disallow import cycles formed by re-export statements (export * / export { } from).deslop/unused-class-member: Flag a public/protected class method, property, or accessor that is declared but never referenced anywhere in the codebase.deslop/unused-dependency: Flag a package.json dependencies entry that no scanned source file imports as an unused dependency (deslop detectStalePackages, UnusedDependency name/isDevDependency=false).deslop/unused-dev-dependency: Flag a devDependencies entry (isDevDependency=true) never imported by any scanned source file.deslop/unused-enum-member: Flag an enum member that is declared but never referenced anywhere in the project (e.g. `enum Color { Red, Green, Blue }` where `Color.Blue` is never used).deslop/unused-export: Flag a named or default value export (isTypeOnly=false) that no other module in the project imports.deslop/unused-file: Flag a source file unreachable from any configured entry point (deslop unusedFiles / detectOrphanFiles), likely dead — delete it.deslop/unused-type: Disallow exporting a type/interface that no other module imports (opt-in, requires reportTypes).deslop/unused-type-declaration: Disallow exported type declarations (interface / type-alias / enum-type) that are never referenced anywhere in the project.
Next.js
react-doctor/nextjs-async-client-component: Fetch data in a parent Server Component and pass it as props, or use useQuery/useSWR in the client componentreact-doctor/nextjs-image-missing-sizes: Add sizes for responsive behavior: `sizes="(max-width: 768px) 100vw, 50vw"` matching your layout breakpointsreact-doctor/nextjs-inline-script-missing-id: Add `id="descriptive-name"` so Next.js can track, deduplicate, and re-execute the script correctlyreact-doctor/nextjs-missing-metadata: Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`react-doctor/nextjs-no-a-element: `import Link from 'next/link'` — enables client-side navigation, prefetching, and preserves scroll positionreact-doctor/nextjs-no-client-fetch-for-server-data: Remove 'use client' and fetch directly in the Server Component — no API round-trip, secrets stay on serverreact-doctor/nextjs-no-client-side-redirect: Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)react-doctor/nextjs-no-css-link: Import CSS directly: `import './styles.css'` or use CSS Modules: `import styles from './Button.module.css'`react-doctor/nextjs-no-font-link: `import { Inter } from "next/font/google"` — self-hosted, zero layout shift, no render-blocking requestsreact-doctor/nextjs-no-head-import: Use the Metadata API instead: `export const metadata = { title: '...' }` or `export async function generateMetadata()`react-doctor/nextjs-no-img-element: `import Image from 'next/image'` — provides automatic WebP/AVIF, lazy loading, and responsive srcsetreact-doctor/nextjs-no-native-script: `import Script from "next/script"` — use `strategy="afterInteractive"` for analytics or `"lazyOnload"` for widgetsreact-doctor/nextjs-no-polyfill-script: Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automaticallyreact-doctor/nextjs-no-redirect-in-try-catch: Move the redirect/notFound call outside the try block, or add `unstable_rethrow(error)` in the catchreact-doctor/nextjs-no-use-search-params-without-suspense: Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`
Performance
react-doctor/advanced-event-handler-refs: Store the handler in a ref and have the listener read `handlerRef.current()` — the subscription stays put while the latest handler is always calledreact-doctor/async-await-in-loop: Collect the items and use `await Promise.all(items.map(...))` to run independent operations concurrentlyreact-doctor/async-defer-await: Move the `await` after the synchronous early-return guard so the skip path stays fastreact-doctor/async-parallel: Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrentlyreact-doctor/client-passive-event-listeners: Add `{ passive: true }` as the third argument: `addEventListener('scroll', handler, { passive: true })`. Only do this if the handler does NOT call `event.preventDefault()` — passive listeners silently ignore `preventDefault()`, which breaks features like pull-to-refresh suppression, custom gestures, and nested-scroll containment.react-doctor/js-async-reduce-without-awaited-acc: Await the accumulator inside an async .reduce reducer: const acc = await previous; ...; return acc;react-doctor/js-batch-dom-css: Batch DOM/CSS reads and writes — interleaving them inside a loop causes layout thrashing. Read first, then writereact-doctor/js-cache-property-access: Hoist the deep member access into a const at the top of the loop body: `const { x, y } = obj.deeply.nested`react-doctor/js-cache-storage: Cache repeated `localStorage`/`sessionStorage` reads in a local variable — each access serializes/deserializesreact-doctor/js-combine-iterations: Combine `.map().filter()` (or similar chains) into a single pass with `.reduce()` or a `for...of` loop to avoid iterating the array twicereact-doctor/js-early-exit: Add an early `return` / `continue` to flatten deep nesting and short-circuit when the predicate is already knownreact-doctor/js-flatmap-filter: Use `.flatMap(item => condition ? [value] : [])` — transforms and filters in a single pass instead of creating an intermediate arrayreact-doctor/js-hoist-intl: Hoist `new Intl.NumberFormat(...)` to module scope or wrap in `useMemo` — Intl constructors allocate dozens of objects per locale lookupreact-doctor/js-hoist-regexp: Hoist `new RegExp(...)` (or large regex literals) to a module-level constant so it isn't recompiled on every loop iterationreact-doctor/js-index-maps: Build an index `Map` once outside the loop instead of `array.find(...)` inside itreact-doctor/js-length-check-first: Short-circuit with `a.length === b.length && a.every((x, i) => x === b[i])` — unequal-length arrays exit immediatelyreact-doctor/js-min-max-loop: Use `Math.min(...array)` / `Math.max(...array)` instead of sorting just to read the first or last elementreact-doctor/js-set-map-lookups: Use a `Set` or `Map` for repeated membership tests / keyed lookups — `Array.includes`/`find` is O(n) per callreact-doctor/js-tosorted-immutable: Use `array.toSorted()` (ES2023) instead of `[...array].sort()` for immutable sorting without the spread allocationreact-doctor/jsx-no-constructed-context-values: Memoize the context value with useMemo/useCallback or hoist it outside the renderreact-doctor/jsx-no-jsx-as-prop: Hoist the inline JSX out of render or memoize it with useMemo so the prop value is stable across renders.react-doctor/jsx-no-new-array-as-prop: Memoize the array (useMemo) or hoist it outside the component instead of allocating a new one each render.react-doctor/jsx-no-new-function-as-prop: Memoize the callback (useCallback) or hoist it outside the component to keep a stable reference across renders.react-doctor/jsx-no-new-object-as-prop: Memoize the object (useMemo) or hoist it outside the component.react-doctor/no-array-index-key: Use a stable, data-derived key instead of the array iteration index.react-doctor/no-flush-sync: Use startTransition for non-urgent updates — flushSync forces a sync flush that skips View Transitions and concurrent renderingreact-doctor/no-global-css-variable-animation: Set the variable on the nearest element instead of a parent, or use `@property` with `inherits: false` to prevent cascade. Better yet, use targeted `element.style.transform` updatesreact-doctor/no-inline-bounce-easing: Use `cubic-bezier(0.16, 1, 0.3, 1)` (ease-out-expo) for natural deceleration — objects in the real world don't bouncereact-doctor/no-inline-prop-on-memo-component: Hoist the inline `() => ...` / `[]` / `{}` to a stable reference (useMemo, useCallback, or module scope) so the memoized child doesn't re-render every parent renderreact-doctor/no-large-animated-blur: Keep blur radius under 10px, or apply blur to a smaller element. Large blurs multiply GPU memory usage with layer sizereact-doctor/no-layout-property-animation: Use `transform: translateX()` or `scale()` instead — they run on the compositor and skip layout/paintreact-doctor/no-layout-transition-inline: Use `transform` and `opacity` for transitions — they run on the compositor thread. For height animations, use `grid-template-rows: 0fr → 1fr`react-doctor/no-long-transition-duration: Keep UI transitions under 1s — 100-150ms for instant feedback, 200-300ms for state changes, 300-500ms for layout changes. Use longer durations only for page-load hero animationsreact-doctor/no-permanent-will-change: Add will-change on animation start (`onMouseEnter`) and remove on end (`onAnimationEnd`). Permanent promotion wastes GPU memory and can degrade performancereact-doctor/no-scale-from-zero: Use `initial={{ scale: 0.95, opacity: 0 }}` — elements should deflate like a balloon, not vanish into a pointreact-doctor/no-transition-all: List specific properties: `transition: "opacity 200ms, transform 200ms"` — or in Tailwind use `transition-colors`, `transition-opacity`, or `transition-transform`react-doctor/no-unstable-nested-components: Hoist nested components to module scope or memoize them — never define one inside another.react-doctor/no-usememo-simple-expression: Remove useMemo — property access, math, and ternaries are already cheap without memoizationreact-doctor/prefer-stable-empty-fallback: Hoist a module-level const EMPTY = [] (or {}) and use it as the || / ?? fallback so the memoised child sees a stable referencereact-doctor/redux-useselector-inline-derivation: Select the raw slice in useSelector and derive with useMemo, or hoist into a memoised createSelector from reselect.react-doctor/redux-useselector-returns-new-collection: useSelector that returns a fresh object/array literal re-renders on every action; return a primitive, split into multiple useSelector calls, or pass shallowEqual.react-doctor/rendering-animate-svg-wrapper: Wrap the SVG: `<motion.div animate={...}><svg>...</svg></motion.div>`react-doctor/rendering-hoist-jsx: Move the static JSX to module scope: `const ICON = <svg>...</svg>` outside the component so it isn't recreated each renderreact-doctor/rendering-hydration-no-flicker: Use `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` or add `suppressHydrationWarning` to the elementreact-doctor/rendering-script-defer-async: Add `defer` for DOM-dependent scripts or `async` for independent ones (analytics). In Next.js, use `<Script strategy="afterInteractive" />` insteadreact-doctor/rendering-svg-precision: Truncate path/points/transform decimals to 1–2 digits — sub-pixel precision adds bytes with no visible differencereact-doctor/rendering-usetransition-loading: Replace with `const [isPending, startTransition] = useTransition()` — avoids a re-render for the loading statereact-doctor/rerender-defer-reads-hook: Read the URL state inside the handler (e.g. `new URL(window.location.href).searchParams`) so the component doesn't subscribe and re-render on every URL changereact-doctor/rerender-derived-state-from-hook: Use a threshold/media-query hook (e.g. `useMediaQuery("(max-width: 767px)")`) — the component re-renders only when the threshold flips, not every pixelreact-doctor/rerender-functional-setstate: Use the callback form: `setState(prev => prev + 1)` to always read the latest valuereact-doctor/rerender-lazy-ref-init: Lazy-init the ref: `const ref = useRef(null); if (ref.current === null) ref.current = expensiveCall()`react-doctor/rerender-lazy-state-init: Wrap in an arrow function so it only runs once: `useState(() => expensiveComputation())`react-doctor/rerender-memo-before-early-return: Extract the JSX into a memoized child component so the parent's early return short-circuits before the child rendersreact-doctor/rerender-memo-with-default-value: Move to module scope: `const EMPTY_ITEMS: Item[] = []` then use as the default valuereact-doctor/rerender-state-only-in-handlers: Replace useState with useRef when the value is only mutated and never read in render — `ref.current = ...` updates without re-rendering the componentreact-doctor/rerender-transitions-scroll: Wrap the setState in startTransition (mark as non-urgent), use useDeferredValue, or stash in a ref + rAF throttle so scroll/pointer events don't trigger a re-render per firereact-doctor/tanstack-start-loader-parallel-fetch: Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to avoid request waterfalls in route loaders
Preact
react-doctor/preact-no-children-length: Wrap with toChildArray(children) from preact before reading .length or calling array methods on props.children.react-doctor/preact-no-react-hooks-import: Import hooks from `preact/hooks` (or `preact/compat`), not `react`: import { useState } from "preact/hooks"react-doctor/preact-no-render-arguments: Drop render's positional params and read this.props / this.state inside render() insteadreact-doctor/preact-prefer-ondblclick: Rename onDoubleClick to onDblClick on host elements: <li onDblClick={openInline}> — Preact uses DOM event namesreact-doctor/preact-prefer-oninput: Replace onChange with onInput on text-like inputs: onInput={(e) => setQuery(e.currentTarget.value)}
React Compiler
react-hooks-js/component-hook-factories: Deprecated: this rule has been removed in 7.1.0.react-hooks-js/error-boundaries: Validates usage of error boundaries instead of try/catch for errors in child componentsreact-hooks-js/globals: Validates against assignment/mutation of globals during render, part of ensuring that [side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)react-hooks-js/hooks: Validates the rules of hooksreact-hooks-js/immutability: Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)react-hooks-js/incompatible-library: Validates against usage of libraries which are incompatible with memoization (manual or automatic)react-hooks-js/preserve-manual-memoization: Validates that existing manual memoized is preserved by the compiler. React Compiler will only compile components and hooks if its inference [matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)react-hooks-js/purity: Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functionsreact-hooks-js/refs: Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)react-hooks-js/set-state-in-effect: Validates against calling setState synchronously in an effect. This can indicate non-local derived data, a derived event pattern, or improper external data synchronization.react-hooks-js/set-state-in-render: Validates against setting state during render, which can trigger additional renders and potential infinite render loopsreact-hooks-js/static-components: Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-renderingreact-hooks-js/todo: Unimplemented featuresreact-hooks-js/unsupported-syntax: Validates against syntax that we do not plan to support in React Compilerreact-hooks-js/use-memo: Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.react-hooks-js/void-use-memo: Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.
React Native
react-doctor/rn-animate-layout-property: Animate `transform: [{ translateX/Y }, { scale }]` and `opacity` instead of layout props — layout runs on the JS thread; transform/opacity run on the GPU compositorreact-doctor/rn-animation-reaction-as-derived: Replace useAnimatedReaction with `useDerivedValue(() => ..., [deps])` — shorter, native dependency tracking, no side-effect implicationreact-doctor/rn-bottom-sheet-prefer-native: Use `<Modal presentationStyle="formSheet">` (RN v7+) for native gesture handling and snap pointsreact-doctor/rn-list-callback-per-row: Hoist the handler with useCallback at list scope and pass the row id as a primitive prop, so the row's memo() shallow-compare actually hitsreact-doctor/rn-list-data-mapped: Wrap the projection in `useMemo(() => items.map(...), [items])` so the list's `data` prop has a stable reference across parent rendersreact-doctor/rn-list-missing-estimated-item-size: Add estimatedItemSize so FlashList/LegendList sizes its initial container pool correctly: <FlashList data={items} estimatedItemSize={64} />react-doctor/rn-list-recyclable-without-types: Add `getItemType={item => item.kind}` so FlashList keeps separate recycle pools per item type — heterogeneous rows shouldn't share recycled cellsreact-doctor/rn-no-deprecated-modules: Import from the community package instead — deprecated modules were removed from the react-native corereact-doctor/rn-no-dimensions-get: Use `const { width, height } = useWindowDimensions()` — it updates reactively on rotation and resizereact-doctor/rn-no-falsy-and-render: Guard numeric-looking conditions: {count > 0 && <X/>}, {Boolean(count) && <X/>}, or {count ? <X/> : null}react-doctor/rn-no-inline-flatlist-renderitem: Extract renderItem to a named function or wrap in useCallback to avoid re-creating on every renderreact-doctor/rn-no-inline-object-in-list-item: Hoist style/object props outside renderItem (StyleSheet.create, useMemo at list scope, or pass primitives) so memo() row components stop bailingreact-doctor/rn-no-legacy-expo-packages: Migrate to the recommended replacement package — legacy Expo packages are no longer maintainedreact-doctor/rn-no-legacy-shadow-styles: Use `boxShadow` for cross-platform shadows on the new architecture instead of platform-specific shadow propertiesreact-doctor/rn-no-non-native-navigator: Use `@react-navigation/native-stack` (or `native-tabs` in v7+) for platform-native transitions and gesturesreact-doctor/rn-no-raw-text: Wrap text in a `<Text>` component: `<Text>{value}</Text>` — raw strings outside `<Text>` crash on React Nativereact-doctor/rn-no-renderitem-key: Remove the no-op `key` from the JSX row that renderItem returns and set `keyExtractor` (or `item.key`) on the list instead.react-doctor/rn-no-scroll-state: Track scroll position with a Reanimated shared value (`useAnimatedScrollHandler`) or a ref — `setState` on every scroll event causes re-render stormsreact-doctor/rn-no-scrollview-mapped-list: Use FlashList, LegendList, or FlatList — `<ScrollView>{items.map(...)}</ScrollView>` mounts every row in memoryreact-doctor/rn-no-single-element-style-array: Use `style={value}` instead of `style={[value]}` — single-element arrays add unnecessary allocationreact-doctor/rn-prefer-content-inset-adjustment: Drop the SafeAreaView wrapper and set `contentInsetAdjustmentBehavior="automatic"` on the ScrollView for native safe-area handlingreact-doctor/rn-prefer-expo-image: Use `<Image>` from `expo-image` instead of `react-native` — same prop API, plus disk + memory caching, placeholders, and crossfadesreact-doctor/rn-prefer-pressable: Use `<Pressable>` from react-native (or react-native-gesture-handler) instead of legacy Touchable* componentsreact-doctor/rn-prefer-pressable-over-gesture-detector: Replace tap-only `<GestureDetector>` with `<Pressable>`: <Pressable onPress={onPress}> or createCSSAnimatedComponent(Pressable) for animated press feedbackreact-doctor/rn-prefer-reanimated: Use `import Animated from 'react-native-reanimated'` — animations run on the UI thread instead of the JS threadreact-doctor/rn-pressable-shared-value-mutation: Wrap in <GestureDetector gesture={Gesture.Tap()...}> so the press animation runs on the UI thread instead of bouncing across the JS bridgereact-doctor/rn-scrollview-dynamic-padding: Use `contentInset={{ bottom: dynamicValue }}` — the OS applies it as an offset without reflowing the scroll contentreact-doctor/rn-scrollview-flex-in-content-container: Replace `flex: <positive number>` on contentContainerStyle with `flexGrow: 1`react-doctor/rn-style-prefer-boxshadow: Use the cross-platform CSS `boxShadow` string (RN v7+): `boxShadow: "0 2px 8px rgba(0,0,0,0.1)"` instead of platform-specific shadow*/elevation keys
Security
react-doctor/active-static-asset: A browser-reachable SVG that contains a `<script>` tag or `on*` event handler runs that code in your origin when someone opens it, which can lead to cross-site scripting.react-doctor/agent-tool-capability-risk: An AI agent tool that can reach shell, filesystem, or network primitives lets prompt-injected input trigger those actions, because the model treats tool arguments as trusted.react-doctor/artifact-baas-authority-surface: Shipping Firebase/Supabase client config with your collection and authorization-field names in a browser bundle hands attackers a map of your data model, which is dangerous when server-side rules do not enforce access.react-doctor/artifact-env-leak: A real secret shipped in a browser bundle under a public env prefix (`NEXT_PUBLIC_`, `VITE_`, `REACT_APP_`, `EXPO_PUBLIC_`) is world-readable and must be treated as compromised.react-doctor/artifact-secret-leak: A live credential (API key, token, or connection string) sits in a browser bundle or static asset, so anyone can read it, and it must be treated as compromised.react-doctor/build-pipeline-secret-boundary: Installing dependencies while CI secrets are in the environment lets a malicious package's lifecycle script read those secrets, which risks supply-chain compromise.react-doctor/clickjacking-redirect-risk: A redirect target taken from caller input, or a privileged page that allows untrusted framing, lets attackers send users to malicious sites or trick them through clickjacking.react-doctor/command-execution-input-risk: Passing caller-controlled input into a shell command lets an attacker run arbitrary commands on your server (remote code execution).react-doctor/cors-cookie-trust-risk: Combining credentialed CORS with a wildcard or less-trusted origin, or scoping auth cookies to a parent domain, lets other sites or subdomains ride a user's session.react-doctor/dangerous-html-sink: Passing user- or request-derived data into an HTML sink like `dangerouslySetInnerHTML` or `innerHTML` without sanitizing it allows cross-site scripting.react-doctor/firebase-client-owned-authz-field: When the client writes ownership or role fields (`ownerId`, `orgId`, `role`, `isAdmin`) to Firebase/Supabase, an attacker can forge them and grant themselves access.react-doctor/firebase-permissive-rules: A Firebase rule of `if true` or `if request.auth != null` leaves data open to everyone (or to every signed-in user), treating sign-in as authorization and exposing other users' data.react-doctor/firebase-query-filter-as-auth: Relying on a client-side Firestore `.where('userId', '==', …)` filter for access control is unsafe, because a client can drop the filter and read everyone's data.react-doctor/git-provider-url-injection-risk: Interpolating request input into a Git provider URL without encoding lets an attacker inject extra path segments or parameters and redirect the request.react-doctor/iframe-missing-sandbox: Add sandbox="" (or a curated, minimal set of allow- tokens) to your iframe to restrict embedded content.react-doctor/import-metadata-execution-risk: Evaluating imported metadata or file contents (EXIF, manifests, presets, uploads, archives) as code lets an attacker achieve remote code execution.react-doctor/insecure-crypto-risk: Weak primitives (MD5, SHA-1, DES, RC4), non-timing-safe comparisons, or `Math.random()` for security values make signatures, tokens, and passwords easier to forge or guess.react-doctor/jsx-no-target-blank: Add rel="noreferrer" (or "noopener") whenever using target="_blank".react-doctor/key-lifecycle-risk: A private key or release credential committed inline to the repo is exposed in git history and must be rotated and revoked.react-doctor/local-rpc-native-bridge-risk: A localhost or native bridge that accepts loose origins and exposes install/update or shell commands lets a malicious web page drive native actions on the user's machine.react-doctor/mcp-tool-capability-risk: An MCP tool runs with the connecting client's authority, so reaching shell, filesystem, or network primitives without validation lets injected input abuse them.react-doctor/mdx-ssr-execution-risk: Compiling untrusted MDX with the full pipeline runs attacker-supplied JSX and expressions on your server, which can lead to code execution.react-doctor/nextjs-no-side-effect-in-get-handler: Move the side effect to a POST handler and use a <form> or fetch with method POST — GET requests can be triggered by prefetching and are vulnerable to CSRFreact-doctor/no-eval: Use `JSON.parse` for serialized data, `Function(...)` (still careful) for trusted templates, or refactor to avoid dynamic code executionreact-doctor/no-secrets-in-client-code: Move secrets to server-only code. Public client environment variables are bundled into browser code and must not contain secretsreact-doctor/nosql-injection-risk: Building a NoSQL query from raw client input lets an attacker inject operator-shaped keys or `$where` code and read or alter data they should not.react-doctor/package-metadata-secret: A secret or public-prefixed secret name in `package.json` leaks easily, because package metadata is routinely published to registries, logs, and browser bundles.react-doctor/path-traversal-risk: Building a filesystem path from request input lets an attacker use `..` or absolute paths to read or write files outside the intended directory.react-doctor/plugin-update-trust-risk: Downloading and running an update or plugin without verifying its integrity lets an attacker ship malicious code to your users.react-doctor/postmessage-origin-risk: Reading `event.data` in a `message` handler without checking `event.origin` lets any other window send data your code trusts, which can lead to cross-site scripting or data theft.react-doctor/public-debug-artifact: A browser-reachable debug, log, dump, or report file in your build output can expose source paths, internal routes, env data, or secrets.react-doctor/public-env-secret-name: A public-prefixed env var whose name implies a secret (token, password, private key, service role) is inlined into the client bundle, so a real credential there is world-readable.react-doctor/raw-sql-injection-risk: Building a SQL query by string concatenation or an unsafe raw helper lets an attacker inject SQL and read or modify your database.react-doctor/repository-secret-file: A committed env file, credential, or token is exposed to anyone with repo access and must be rotated, even after you remove it.react-doctor/require-pnpm-hardening: pnpm project is missing supply-chain hardening in pnpm-workspace.yaml — set `minimumReleaseAge`, keep `blockExoticSubdeps: true`, and set `trustPolicy: no-downgrade`react-doctor/supabase-client-owned-authz-field: When the client writes authorization columns (`ownerId`, `orgId`, `role`, `isAdmin`) to Supabase, an attacker can forge them and escalate their own access.react-doctor/supabase-rls-policy-risk: A Supabase policy that disables row-level security, exposes the service role, or uses a `(true)` write predicate lets clients read or modify data that is not theirs.react-doctor/svg-filter-clickjacking-risk: Applying CSS or SVG filters over a cross-origin iframe can be used for clickjacking or to read pixels from framed content the attacker should not see.react-doctor/tanstack-start-get-mutation: Use `createServerFn({ method: 'POST' })` for data modifications — GET requests can be triggered by prefetching and are vulnerable to CSRFreact-doctor/tanstack-start-no-secrets-in-loader: Loaders are isomorphic (run on both server and client). Wrap secret access in `createServerFn()` so it stays server-onlyreact-doctor/tenant-static-proxy-risk: Building an asset path from a client-supplied tenant, subdomain, or workspace value lets one tenant read another tenant's files.react-doctor/untrusted-redirect-following: Following a redirect from a request-supplied URL without re-validating each hop lets an attacker bounce your server into internal addresses (server-side request forgery).react-doctor/url-prefilled-privileged-action: Reading a privileged action from the URL (invite, role, permission, redirect, sharing) and acting on it lets an attacker craft a link that performs that action for a victim.react-doctor/webhook-signature-risk: An inbound webhook handler that acts on the request body without verifying the provider's signature will process forged requests from anyone.socket/low-supply-chain-score: A direct dependency's worst Socket security axis (supply chain or vulnerability) scores below the configured minimum — bump it to a patched/healthier release, replace it, or vet it and raise `supplyChain.minScore`
Server
react-doctor/server-after-nonblocking: `import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blockedreact-doctor/server-auth-actions: Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data accessreact-doctor/server-cache-with-object-literal: Pass primitives to React.cache()-wrapped functions — argument identity (not deep equality) is the dedup key, so a fresh `{}` per render bypasses the cachereact-doctor/server-dedup-props: Pass the source array once and derive the projection on the client — passing both doubles RSC serialization bytesreact-doctor/server-fetch-without-revalidate: Pass `{ next: { revalidate: <seconds> } }` (or `cache: "no-store"` / `next: { tags: [...] }`) so stale cached data doesn't silently persistreact-doctor/server-hoist-static-io: Hoist the read to module scope: `const FONT_DATA = await fetch(new URL('./fonts/Inter.ttf', import.meta.url)).then(r => r.arrayBuffer())` runs once at module loadreact-doctor/server-no-mutable-module-state: Move per-request data into the action body, headers/cookies, or a request-scope (React.cache, AsyncLocalStorage). Module-scope `let`/`var` is shared across requests.react-doctor/server-sequential-independent-await: Wrap independent awaits in `Promise.all([...])` so they race instead of waterfalling — second call doesn't depend on the first
State & Effects
effect/no-adjust-state-on-prop-change: Disallow adjusting state in an effect when a prop changes.effect/no-chain-state-updates: Disallow chaining state changes in an effect.effect/no-derived-state: Disallow storing derived state in an effect.effect/no-event-handler: Disallow using state and an effect as an event handler.effect/no-initialize-state: Disallow initializing state in an effect.effect/no-pass-data-to-parent: Disallow passing data to parents in an effect.effect/no-pass-live-state-to-parent: Disallow passing live state to parents in an effect.effect/no-reset-all-state-on-prop-change: Disallow resetting all state in an effect when a prop changes.react-doctor/activity-wraps-effect-heavy-subtree: Audit the `<Activity>` subtree: every hide/show cycle tears down and recreates every `useEffect`/`useLayoutEffect` inside, so move subscriptions and effect-driven setState chains outside the boundary or pre-resolve the data above it.react-doctor/effect-needs-cleanup: Return a cleanup function that releases the subscription / timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` / `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already returned onereact-doctor/hooks-no-nan-in-deps: Remove the literal NaN from the dependency array, or normalise it (Number.isNaN(x) ? 0 : x) before passing it in.react-doctor/jotai-derived-atom-returns-fresh-object: Split the derivation into per-field primitive derived atoms, or wrap with selectAtom(source, fn, shallow) from jotai/utils when a wrapper object is required.react-doctor/jotai-select-atom-in-render-body: Lift selectAtom to module scope, or wrap it: const a = useMemo(() => selectAtom(base, fn), [deps])react-doctor/jotai-tq-use-raw-query-atom: Derive the field once, then subscribe to the derived atom: const dataAtom = atom((get) => get(queryAtom).data)react-doctor/no-cascading-set-state: Combine into useReducer: `const [state, dispatch] = useReducer(reducer, initialState)`react-doctor/no-derived-state-effect: For derived state, compute inline: `const x = fn(dep)`. For state resets on prop change, use a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effectreact-doctor/no-derived-use: Don't pass a promise created during render into use(); create it in a Server Component (or a stable cache) so its reference stays stable across rendersreact-doctor/no-derived-useState: Remove useState and compute the value inline: `const value = transform(propName)`react-doctor/no-did-mount-set-state: Derive state in getDerivedStateFromProps or initial state instead of calling this.setState in componentDidMount, which forces an extra render.react-doctor/no-did-update-set-state: Avoid calling this.setState in componentDidUpdate; derive the value with getDerivedStateFromProps to prevent re-render loopsreact-doctor/no-direct-state-mutation: Replace the mutation with a setter call that produces a new reference: `setItems([...items, newItem])`, `setItems(items.filter(x => x !== target))`, `setItems(items.toSorted(...))`. React only re-renders on a new reference, so in-place updates are silently droppedreact-doctor/no-effect-chain: Compute as much as possible during render (e.g. `const isGameOver = round > 5`) and write all related state inside the event handler that originally fires the chain. Each effect link adds an extra render and makes the code rigid as requirements evolvereact-doctor/no-effect-event-handler: Move the conditional logic into onClick, onChange, or onSubmit handlers directlyreact-doctor/no-effect-event-in-deps: Call the useEffectEvent callback inside the effect body without listing it; its identity is intentionally unstablereact-doctor/no-effect-with-fresh-deps: Move the constructed value into the hook body and depend on its primitive inputs, or memoize it with useMemo/useCallback so its reference is stable.react-doctor/no-event-trigger-state: Delete the trigger state (`useState(null)` plus the `useEffect` that watches it) and call the side-effect (`post(...)` / `navigate(...)` / `track(...)`) directly inside the event handler that previously called the setter. State should not exist purely to schedule effect runsreact-doctor/no-fetch-in-effect: Use `useQuery()` from @tanstack/react-query, `useSWR()`, or fetch in a Server Component insteadreact-doctor/no-mirror-prop-effect: Delete both the `useState` and the `useEffect` and read the prop directly during render. Mirroring a prop into local state forces a stale first render before the effect re-syncsreact-doctor/no-mutable-in-deps: Read mutable values (`location.pathname`, `ref.current`) inside the effect body instead of in the deps array, or subscribe with `useSyncExternalStore`. Mutations to these don't trigger re-renders, so listing them in deps doesn't make the effect react to changesreact-doctor/no-mutating-reducer-state: Return a new reducer state object/array/collection instead of mutating the current state and returning the same top-level reference.react-doctor/no-prop-callback-in-effect: Lift the shared state into a Provider so both sides read the same source — no useEffect-driven sync neededreact-doctor/no-self-updating-effect: Break the self-updating-effect feedback loop: derive the value during render, move the write into an event handler, or guard the update so it provably converges.react-doctor/no-set-state-in-render: Move the setter call into a `useEffect`, an event handler, or replace the state with a value computed during render. Calling a setter at render time triggers another render, which calls the setter again — an infinite loopreact-doctor/no-will-update-set-state: Don't call this.setState in componentWillUpdate — move the update to getDerivedStateFromProps or componentDidUpdate.react-doctor/prefer-use: Replace useContext(Context) with the React 19 use(Context) API, which reads the same value but may be called conditionallyreact-doctor/prefer-use-effect-event: Wrap the callback with `useEffectEvent(callback)` (React 19+) and call the resulting binding from inside the sub-handler. The Effect Event captures the latest props/state without being a reactive dep, so the effect doesn't re-subscribe on every parent render. See https://react.dev/reference/react/useEffectEventreact-doctor/prefer-use-sync-external-store: Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook handles tearing during concurrent renders and SSR snapshots; the manual subscribe pattern doesn'treact-doctor/prefer-useReducer: Group related state: `const [state, dispatch] = useReducer(reducer, { field1, field2, ... })`react-doctor/rerender-dependencies: Extract to a useMemo, useRef, or module-level constant so the reference is stable
TanStack Query
react-doctor/query-mutation-missing-invalidation: Add `onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] })` so cached data stays in sync after the mutationreact-doctor/query-no-query-in-effect: React Query manages refetching automatically via queryKey dependencies and the `enabled` option — manual refetch() in useEffect is usually unnecessaryreact-doctor/query-no-rest-destructuring: Destructure only the fields you need: `const { data, isLoading } = useQuery(...)` — rest destructuring subscribes to all fields and causes extra re-rendersreact-doctor/query-no-usequery-for-mutation: Use `useMutation()` for POST/PUT/DELETE — it provides onSuccess/onError callbacks, doesn't auto-refetch, and correctly models write operationsreact-doctor/query-no-void-query-fn: queryFn must return a value for the cache. Use the `enabled` option to conditionally disable the query instead of returning undefinedreact-doctor/query-stable-query-client: Move `new QueryClient()` to module scope or wrap in `useState(() => new QueryClient())` — recreating it on every render resets the entire cache
TanStack Start
react-doctor/tanstack-start-missing-head-content: Add `<HeadContent />` inside `<head>` in your __root route — without it, route `head()` meta tags are silently droppedreact-doctor/tanstack-start-no-anchor-element: `import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload="intent"`, and client-side navigationreact-doctor/tanstack-start-no-direct-fetch-in-loader: Use `createServerFn()` from @tanstack/react-start — provides type-safe RPC, input validation, and proper server/client code splittingreact-doctor/tanstack-start-no-dynamic-server-fn-import: Use `import { myFn } from '~/utils/my.functions'` — the bundler replaces server code with RPC stubs only for static importsreact-doctor/tanstack-start-no-navigate-in-render: Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader` instead — navigate() during render causes hydration issuesreact-doctor/tanstack-start-no-use-server-in-handler: TanStack Start handles server boundaries automatically via the Vite plugin — "use server" inside createServerFn causes compilation errorsreact-doctor/tanstack-start-no-useeffect-fetch: Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfallsreact-doctor/tanstack-start-redirect-in-try-catch: TanStack Router's `redirect()` and `notFound()` throw special errors caught by the router. Move them outside the try block or re-throw in the catchreact-doctor/tanstack-start-route-property-order: Follow the order: params/validateSearch → loaderDeps → context → beforeLoad → loader → head. See https://tanstack.com/router/latest/docs/eslint/create-route-property-orderreact-doctor/tanstack-start-server-fn-method-order: Chain methods in order: .middleware() → .inputValidator() → .client() → .server() → .handler() — types depend on this sequencereact-doctor/tanstack-start-server-fn-validate-input: Add `.inputValidator(schema)` before `.handler()` — data crosses a network boundary and must be validated at runtime