Skip to content

[eslint-plugin-react-hooks] setState in effect guard prevents cases like DOM measurement #15329

@billyjanitsch

Description

@billyjanitsch

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

Bug

What is the current behavior?

The new guard against a direct call to setState inside of an effect (#15184) seems to prevent a class of patterns where the value being set is dependent on something other than props. For example, the rule disallows storing a value read from the DOM via a ref (see below).

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

function MeasuredButton(props) {
  const buttonRef = useRef(null)
  const [buttonWidth, setButtonWidth] = useState(0)

  useLayoutEffect(() => {
    if (buttonRef.current) {
      // we rely on the same value bailout to avoid an infinite loop
      setButtonWidth(buttonRef.current.clientWidth)
      // we could bail out explicitly instead:
      // const {clientWidth} = buttonRef.current
      // if (clientWidth !== buttonWidth) setButtonWidth(clientWidth)
      // but the linter would still disallow it
    }
  })

  return (
    <>
      <button ref={buttonRef}>{props.children}</button>
      Button width: {buttonWidth}
    </>
  )
}

This code yields the error:

React Hook useLayoutEffect contains a call to 'setButtonWidth'. Without a list of dependencies, this can lead to an infinite chain of updates. To fix this, pass [] as a second argument to the useLayoutEffect Hook.

The auto-fix breaks the component because the width no longer updates on subsequent renders.

What is the expected behavior?

Basically, the guard assumes that the infinite loop problem can always be solved by adding a dependency array. This is true when setting a value derived from props (such as data returned from a request based on a prop), but not when the source of the value can only be retrieved inside of the effect (such as a DOM measurement). In the latter case, an infinite loop has to be avoided by adding a condition or relying on the same value bailout.

Is this known and/or intentional? I notice that #15184 considered early returns, which would help.

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

React: 16.8.6
eslint-plugin-react-hooks: 1.6.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    Resolution: StaleAutomatically closed due to inactivity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions