Rules reference
305 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-tag-over-role— Replace role with the semantic HTML element when one exists.react-doctor/role-supports-aria-props— Use only aria-* props that are supported by the element's explicit or implicit ARIA role.
Architecture
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-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/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/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.
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
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/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-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-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
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-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/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-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
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-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-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-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-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-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/iframe-missing-sandbox— Add sandbox="" (or a curated, minimal set of allow- tokens) to your iframe to restrict embedded content.react-doctor/jsx-no-target-blank— Add rel="noreferrer" (or "noopener") whenever using target="_blank".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/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-only
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/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/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-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-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-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