Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

react-hooks/rules-of-hooks: Improve support for do/while loops #31720

Merged
merged 2 commits into from
Dec 10, 2024

Conversation

tyxla
Copy link
Contributor

@tyxla tyxla commented Dec 10, 2024

Summary

This is an alternative to #28714.

Reverts the main fix there and implements a different solution by adding special handling for DoWhileStatement.

Also adds a unit test that was breaking the previous fix (props to @skratchdot for it).

Please, refer to the inline comments for more information.

Fixes #31687. Also see #28714 (comment).

How did you test this change?

I've added unit tests that cover the case and verified that they pass by running:

yarn test packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js --watch

I've also verified that the rest of the tests continue to pass by running:

yarn test

and

yarn test --prod

Copy link

vercel bot commented Dec 10, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-compiler-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 10, 2024 1:31pm

Comment on lines +557 to +558
const [state, setState] = useState(0);
for (let i = 0; i < 10; i++) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the hook is called in a valid way, outside of the loop.

Comment on lines 103 to 112
function isInsideDoWhileLoop(node) {
while (node) {
if ( node.type === 'DoWhileStatement' ) {
return true;
}
node = node.parent;
}
return false;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding an extra utility to traverse up the tree to check if the node is called within a do/while loop.

Copy link
Contributor

@skratchdot skratchdot Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yesterday was the first time i looked at the code in this plugin. i'm curious why all the "is hook inside loop" logic isn't handled this way. seems like the same could be done for all types of loops/cycles and invalid code. i found the "counting segments" code somewhat hard to follow and this function is much more clear to me. maybe the counting segments was done for performance reasons?

without fully understanding the existing code, it seems like this plugin could be rewritten so that we first find all "hook" functions, then call a "isInsideCycle" that climbs up the tree looking for nodes like DoWhileStatement, IfStatement, or ForStatement etc.

anyways, good work on this (and thanks)!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - it does feel quite complex at first glance, and my initial expectation was that these would be handled top-down, with loops and control structures at the top, and then function/hook calls at the bottom, but I assumed there was a good reason (likely performance) for it to be that way. Anyway, rewriting that would be a major overhaul of the plugin, and one definitely outside of this PR's scope.

@@ -295,7 +305,7 @@ export default {
if (pathList.has(segment.id)) {
const pathArray = Array.from(pathList);
const cyclicSegments = pathArray.slice(
pathArray.indexOf(segment.id) - 1,
pathArray.indexOf(segment.id) + 1,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverts the change from #28714 as it was introducing false positives (see #31687).

@@ -485,7 +495,7 @@ export default {
for (const hook of reactHooks) {
// Report an error if a hook may be called more then once.
// `use(...)` can be called in loops.
if (cycled && !isUseIdentifier(hook)) {
if ( (cycled || isInsideDoWhileLoop(hook)) && !isUseIdentifier(hook)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're now considering a hook "cycled" also if it's called within a do/while loop.

@@ -520,7 +530,8 @@ export default {
if (
!cycled &&
pathsFromStartToEnd !== allPathsFromStartToEnd &&
!isUseIdentifier(hook) // `use(...)` can be called conditionally.
!isUseIdentifier(hook) && // `use(...)` can be called conditionally.
!isInsideDoWhileLoop(hook) // wrapping do/while loops are checked separately.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically excluding hooks inside do/while calls because we're handling those cases separately in the above check.

@react-sizebot
Copy link

react-sizebot commented Dec 10, 2024

Comparing: 372ec00...f382739

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB +0.11% 1.82 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 510.81 kB 510.81 kB = 91.43 kB 91.43 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB +0.05% 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 515.74 kB 515.74 kB = 92.14 kB 92.14 kB
facebook-www/ReactDOM-prod.classic.js = 595.67 kB 595.67 kB = 105.14 kB 105.14 kB
facebook-www/ReactDOM-prod.modern.js = 585.93 kB 585.93 kB = 103.57 kB 103.57 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable-rc/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +0.31% 87.47 kB 87.75 kB +0.30% 14.67 kB 14.71 kB
oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +0.31% 87.47 kB 87.75 kB +0.30% 14.67 kB 14.71 kB
oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +0.31% 87.47 kB 87.75 kB +0.30% 14.67 kB 14.71 kB
oss-stable-rc/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +0.31% 78.20 kB 78.44 kB +0.25% 14.33 kB 14.37 kB
oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +0.31% 78.20 kB 78.44 kB +0.25% 14.33 kB 14.37 kB
oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +0.31% 78.20 kB 78.44 kB +0.25% 14.33 kB 14.37 kB
oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +0.31% 89.55 kB 89.82 kB +0.29% 14.92 kB 14.97 kB
oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +0.30% 80.06 kB 80.30 kB +0.28% 14.58 kB 14.62 kB

Generated by 🚫 dangerJS against cbb5ecd

@tyxla
Copy link
Contributor Author

tyxla commented Dec 10, 2024

@mofeiZ or @josephsavona - could I please ask for a review of this fix? Thanks in advance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: [eslint-plugin-react-hooks] incorrectly reports an error when hook is called outside of a loop.
5 participants