fix(linter/react/rules-of-hooks): detect constructor callbacks (#25377)

## Summary

- Report hooks called from anonymous functions passed to constructors, including `new Promise.then(...)` inside a component.
- Keep the matching lower-case `notAComponent` case allowed, aligning with `eslint-plugin-react-hooks`.

AI-assisted change; contributor review is requested before merge.
This commit is contained in:
camc314 committed 2026-08-07 22:07:52 +00:00
1 parent e3f6263178
commit 6846a9aa85
2 files changed
+28 -13

No files matched your search

@@ -865,7 +865,7 @@ fn is_reference_call_callee(nodes: &AstNodes<'_>, node_id: NodeId, span: Span) -
})
}
/// Checks if the `node_id` is a callback argument (including JSX render props),
/// Checks if the `node_id` is a callback argument (including constructor and JSX render props),
/// And that function isn't a `React.memo` or `React.forwardRef`.
/// Returns `true` if this node is a function argument/render prop and that isn't a React special function.
/// Otherwise it would return `false`.
@@ -877,8 +877,8 @@ fn is_non_react_func_arg(nodes: &AstNodes, node_id: NodeId) -> bool {
AstKind::CallExpression(call) => {
!(is_react_function_call(call, "forwardRef") || is_react_function_call(call, "memo"))
}
// Callback passed as JSX expression: <Foo>{() => { ... }}</Foo> or <Foo render={() => { ... }} />
AstKind::JSXExpressionContainer(_) => true,
// Callback passed as an argument to a constructor or JSX render prop.
AstKind::NewExpression(_) | AstKind::JSXExpressionContainer(_) => true,
_ => false,
}
}
@@ -1714,6 +1714,14 @@ fn test() {
return null;
}
",
// This matches eslint-plugin-react-hooks: callbacks in non-components are not reported.
r"
function notAComponent() {
return new Promise.then(() => {
useState();
});
}
",
];
let pass_additional_effect_hooks = vec![(
@@ -2459,16 +2467,14 @@ fn test() {
}
}
",
// TODO: This should error but doesn't.
// Original rule also fails to raise this error.
// errors: [genericError('useState')],
// "
// function notAComponent() {
// return new Promise.then(() => {
// useState();
// });
// }
// " ,
// Invalid because hooks cannot be called inside constructor callbacks.
r"
function Component() {
return new Promise.then(() => {
useState();
});
}
",
// https://github.com/oxc-project/oxc/issues/6651
r"const MyComponent3 = makeComponent(function foo () { useHook(); });",
// https://github.com/oxc-project/oxc/issues/17961
@@ -1119,6 +1119,15 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Move the Hook call before the condition, or call it unconditionally and branch inside the Hook/effect instead.
⚠ react-hooks(rules-of-hooks): React Hook "useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.
╭─[rules_of_hooks.tsx:4:21]
3 │ return new Promise.then(() => {
4 │ useState();
· ─────┬────
· ╰── This Hook call is inside a nested callback.
5 │ });
╰────
⚠ react-hooks(rules-of-hooks): React Hook "useHook" is called in function "foo" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".
╭─[rules_of_hooks.tsx:1:54]
1 │ const MyComponent3 = makeComponent(function foo () { useHook(); });