[eslint-plugin-react-hooks] setState in effect guard prevents cases like DOM measurement
See original GitHub issueDo 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 (https://github.com/facebook/react/pull/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
Issue Analytics
- State:
- Created 4 years ago
- Comments:6 (2 by maintainers)
This can still cause infinite loops if the width of the
<button />
depends onbuttonwidth
. It would create a size explosion if you set the width of the button tobuttonWidth + 20
and then the layout reads a different value, updates and then the cycle starts again. There’s no way that a linter can verify this statically.The other issue is if you add other hooks that change the size of the button during render.
If you think it’s safe you can eslint-ignore it. Otherwise you should probably use ResizeObserver or save the latest measurement in a ref and read from that when necessary.
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!