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) => ...
- Category: Maintainability
- Severity: warn
- Source:
oxlint-plugin-react-doctor - Framework: global
- Enabled when: always
- Tags: test-noise
- Default: Enabled
Validation prompt
Use this to decide whether a fired diagnostic is real or a false positive.
Fires on a named non-PascalCase function binding declared inside a component or custom-hook body — a VariableDeclarator whose init is an ArrowFunctionExpression or FunctionExpression (incl. async/generator), or a FunctionDeclaration — whose closureCaptures resolve only to its own params, module imports, module-scope consts, or globals (console, setTimeout, Math, fetch resolve to no symbol and so pass), i.e. it captures NO binding inside the component body scope. The ONLY CallExpression exclusion is the VariableDeclarator init guard, which returns early when a binding's own init is a CallExpression — so const f = useCallback(...) / const f = useMemo(...) and a const X = memo(...) binding are skipped because their init is the call itself; there is NO skip for a helper defined INSIDE a memo()- or forwardRef()-wrapped named function component, so const App = memo(function App(){ const formatName = ... }) and forwardRef(function Input(){ const validate = ... }) DO fire (the wrapper call is traversed transparently and the inner helper is flagged). MemberExpression assignments (Component.helper = ...) and PascalCase bindings (handled by no-nested-component-definition) are skipped. False positive to suppress: the helper actually does close over local state — it reads a useState value or setter (increment = () => setCount(count + 1)), a prop (getCount = () => items.length), or another local helper that itself captures state (doubleCount calls getCount which reads count); or the enclosing PascalCase function is really an object-returning factory/adapter (DailyVideoApiAdapter, AIHandlePlugin returning { ... }) not a component, or a plain non-component function like makeApi.
Fix prompt
Use this once validation confirms the diagnostic is real.
Move the function out of the component to module scope (above the component declaration) since it references no local state, turning a per-render allocation into a one-time module binding: lift const formatName = (user) => user.firstName + " " + user.lastName above function App() unchanged, and if it needs render-time values pass them as explicit parameters rather than closing over them. Only confirm after verifying every identifier the body uses is a param, a module import/const, or a global; if it touches props or useState values, leave it in place.
Related rules
More Maintainability rules from the rules reference:
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-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 directly