React: eslint-plugin-react-hooks: 'Hook is being called conditionally' error outside condition

Created on 19 Sep 2019  ·  4Comments  ·  Source: facebook/react

Do you want to request a feature or report a bug?

BUG (possibly)

What is the current behavior?

The plugin is showing this error:

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? (react-hooks/rules-of-hooks)eslint

But I don't think I'm calling any hooks conditionally.

The code:

https://codesandbox.io/s/exciting-bhabha-mqj7q

function App(props) {
  const someObject = { propA: true, propB: false };

  for (const propName in someObject) {
    if (propName === true) {
      console.log("something");
    } else {
      console.log("whatever");
    }
  }

  // THE PLUGIN ERROR MSG ON THIS useState
  const [myState, setMyState] = useState(null);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

image

What is the expected behavior?

The plugin wouldn't show the error in this situation.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

image

ESLint Rules Bug

Most helpful comment

All 4 comments

I noticed that for this particular example:

pathsFromStartToEnd is 3
allPathsFromStartToEnd is 2

also possiblyHasEarlyReturn is true


This particular error is reported when pathsFromStartToEnd and allPathsFromStartToEnd are unequal.

See RulesOfHooks.js line 404


If you abstract the code inside of the for..in loop into a separate function, the lint error goes away.

In this case, pathsFromStartToEnd and allPathsFromStartToEnd both equal 2:

  const someObject = { propA: true, propB: false };
  const someFunction = (propName) => {
    if (propName === true) {
      console.log("something");
    } else {
      console.log("whatever");
    }
  }
  for (const propName in someObject) {
    someFunction(propName)
  }
  const [myState, setMyState] = useState(null);

I'm still not sure why.

@gaearon, I created an MR addressing and fixing this issue (#16853).

The main reason this happens is that the value of allPathsFromStartToEnd is calculated wrong.
In this example, the number of paths from start to end should be 3, but the function returns 2.

The reason this happens is because of flawed caching of cyclic paths. So in normal graph traversing, we need the path history as well, but it only takes a list of cyclic elements, and the elements leading to this path are not known.
As a result, when a condition is used in a for...in obj, based on the direction we might lose some paths.

This is still an issue with 2.4.0, a little bit simpler repro:

import * as React from "react";

function Component() {
  let isLastEven = false;
  for (let x of [1, 2, 3]) {
    if (x % 2 == 0) {
      isLastEven = true;
    } else {
      isLastEven = false;
    }
  }
  let y = React.useMemo(() => 1, []);
  return <div>{y}</div>;
}

can #16853 be re-openned?

Was this page helpful?
0 / 5 - 0 ratings