Skip to content

Commit c9575f6

Browse files
committed
fix(linter): fix false positive in react/exhaustive deps (#10727)
Fix React exhaustive-deps rule to correctly handle useCallback references This PR fixes the `exhaustive-deps` rule to no longer consider `useCallback` as a stable value, which was causing incorrect linting behavior. The change: 1. Removes `useCallback` from the stable value check, as it should not be treated like `useRef` 2. Updates tests to reflect this change by: - Commenting out tests that would cause infinite loops at runtime - Uncommenting a test case that should now pass - Adding a test case for the specific issue reported in #9788 closes #9788 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved handling of React hooks by updating which hooks are recognized as stable, specifically adjusting support for useCallback. - **Tests** - Updated test coverage: disabled tests for unsupported recursive useCallback cases, enabled a nested callback test, and added a new test case related to a known issue. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 62843e6 commit c9575f6

File tree

2 files changed

+53
-23
lines changed

2 files changed

+53
-23
lines changed

crates/oxc_linter/src/rules/react/exhaustive_deps.rs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,7 @@ fn is_stable_value<'a, 'b>(
893893
return false;
894894
};
895895

896-
if init_name == "useRef" || init_name == "useCallback" {
896+
if init_name == "useRef" {
897897
return true;
898898
}
899899

@@ -2009,18 +2009,20 @@ fn test() {
20092009
</>
20102010
);
20112011
}",
2012-
r"function Example() {
2013-
const foo = useCallback(() => {
2014-
foo();
2015-
}, []);
2016-
}",
2017-
r"function Example({ prop }) {
2018-
const foo = useCallback(() => {
2019-
if (prop) {
2020-
foo();
2021-
}
2022-
}, [prop]);
2023-
}",
2012+
// we don't support the following two cases as they would both cause an infinite loop at runtime
2013+
// r"function Example() {
2014+
// const foo = useCallback(() => {
2015+
// foo();
2016+
// }, []);
2017+
// }",
2018+
//
2019+
// r"function Example({ prop }) {
2020+
// const foo = useCallback(() => {
2021+
// if (prop) {
2022+
// foo();
2023+
// }
2024+
// }, [prop]);
2025+
// }",
20242026
r"function Hello() {
20252027
const [state, setState] = useState(0);
20262028
useEffect(() => {
@@ -3324,21 +3326,20 @@ fn test() {
33243326
r"function Thing() {
33253327
useEffect(async () => {});
33263328
}",
3327-
// TODO: not supported yet
3329+
// NOTE: intentionally not supported, as `foo` would be referenced before it's declaration
33283330
// r"function Example() {
33293331
// const foo = useCallback(() => {
33303332
// foo();
33313333
// }, [foo]);
33323334
// }",
3333-
// TODO: not supported yet
3334-
// r"function Example({ prop }) {
3335-
// const foo = useCallback(() => {
3336-
// prop.hello(foo);
3337-
// }, [foo]);
3338-
// const bar = useCallback(() => {
3339-
// foo();
3340-
// }, [foo]);
3341-
// }",
3335+
r"function Example({ prop }) {
3336+
const foo = useCallback(() => {
3337+
prop.hello(foo);
3338+
}, [foo]);
3339+
const bar = useCallback(() => {
3340+
foo();
3341+
}, [foo]);
3342+
}",
33423343
r"function MyComponent() {
33433344
const local = {};
33443345
function myEffect() {
@@ -3563,6 +3564,17 @@ fn test() {
35633564
<></>
35643565
)
35653566
}",
3567+
// https://github.com/oxc-project/oxc/issues/9788
3568+
r#"import { useCallback, useEffect } from "react";
3569+
3570+
function Component({ foo }) {
3571+
const log = useCallback(() => {
3572+
console.log(foo);
3573+
}, [foo]);
3574+
useEffect(() => {
3575+
log();
3576+
}, []);
3577+
}"#,
35663578
];
35673579

35683580
Tester::new(ExhaustiveDeps::NAME, ExhaustiveDeps::PLUGIN, pass, fail).test_and_snapshot();

crates/oxc_linter/src/snapshots/react_exhaustive_deps.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1907,6 +1907,15 @@ source: crates/oxc_linter/src/tester.rs
19071907
╰────
19081908
help: Consider putting the asynchronous code inside a function and calling it from the effect.
19091909
1910+
⚠ eslint-plugin-react-hooks(exhaustive-deps): React Hook useCallback has a missing dependency: 'prop'
1911+
╭─[exhaustive_deps.tsx:4:14]
1912+
3 │ prop.hello(foo);
1913+
4 │ }, [foo]);
1914+
· ─────
1915+
5const bar = useCallback(() => {
1916+
╰────
1917+
help: Either include it or remove the dependency array.
1918+
19101919
eslint-plugin-react-hooks(exhaustive-deps): React Hook useEffect has a missing dependency: 'local'
19111920
╭─[exhaustive_deps.tsx:6:31]
19121921
5 │ }
@@ -2248,3 +2257,12 @@ source: crates/oxc_linter/src/tester.rs
22482257
11
22492258
╰────
22502259
help: Either include it or remove the dependency array.
2260+
2261+
eslint-plugin-react-hooks(exhaustive-deps): React Hook useEffect has a missing dependency: 'log'
2262+
╭─[exhaustive_deps.tsx:9:12]
2263+
8log();
2264+
9 │ }, []);
2265+
· ──
2266+
10 │ }
2267+
╰────
2268+
help: Either include it or remove the dependency array.

0 commit comments

Comments
 (0)