react-doctor/no-unstable-nested-components
Hoist nested components to module scope or memoize them — never define one inside another.
- Category: Performance
- Severity: warn
- Source:
oxlint-plugin-react-doctor - Framework: global
- Enabled when: always
- Documentation: https://oxc.rs/docs/guide/usage/linter/rules/react/no-unstable-nested-components
Validation prompt
Use this to decide whether a fired diagnostic is real or a false positive.
Fires when a component candidate is defined inside an enclosing React component (or class render method) — a candidate being a PascalCase-named function/class, a function passed as a JSX prop value, or an object-property callback inside a call/array — whose body contains JSX or React.createElement. Excluded: useCallback-wrapped functions, .map/.forEach/.filter/.flatMap/.reduce callbacks, the children prop, render-prop names matching render*, and the first argument of an HoC (memo/forwardRef/lazy/observer — the outer call is reported instead). False positive: with the default allowAsProps:true, a render-prop component passed via a prop (e.g. footer={() => <div/>}) is the canonical composition pattern and is NOT flagged — only inline definitions in the render body that remount on every render are real findings.
Fix prompt
Use this once validation confirms the diagnostic is real.
Move the nested component to module scope so its identity is stable across renders: declare function NestedThing() {...} outside the parent and pass data via props instead of closing over the parent's variables. If it must read parent state and is genuinely small, lift the dynamic value to a prop or memoize with useCallback (const Nested = useCallback(() => <div/>, [deps])) so React preserves the subtree's DOM and state. For render props, pass the component as a prop rather than redefining it inline. See https://oxc.rs/docs/guide/usage/linter/rules/react/no-unstable-nested-components
Related rules
More Performance rules from the rules reference:
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>`