floating-ui / react-popper

🍿⚛Official React library to use Popper, the positioning library
https://popper.js.org/react-popper/
MIT License
2.5k stars 226 forks source link

Warning: flushSync was called from inside a lifecycle method. #458

Open catamphetamine opened 1 year ago

catamphetamine commented 1 year ago

I get the following warning in the console:

Warning: flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.

The warning seems to originate from this flushSync() call: https://github.com/floating-ui/react-popper/blob/beac280d61082852c4efc302be902911ce2d424c/src/usePopper.js#L78

The same issue was encountered in Mantine React component library. They've decided to migrate from react-popper to fix that in that issue.

denisinvader commented 1 year ago

Using Promise.resolve().then(popper.forceUpdate) instead of popper.forceUpdate() solves the issue as mentioned in salute-developers/plasma PR

catamphetamine commented 10 months ago

So looks like the rationale for the suggested workaround is that react-popper's forceUpdate() function, calls flushSync() inside. Therefore, it shouldn't be called in a callback right after the component has re-rendered because that results in a second render right after the first one, and React doesn't like that, presumably for performance reasons. Spacing out those two renders in time via Promise.resolve() seems to fix the React warning.

catamphetamine commented 10 months ago

I've tested the suggested Promise.resolve() approach and it doesn't really work and the warning is still being shown. What worked though is setTimeout(forceUpdate, 0). Not forget that the timeout should be "cleared" on component unmount.

const { styles, attributes, forceUpdate } = usePopper(...)

const updateTooltipPositionTimer = useRef()

// Update tooltip position.
if (updateTooltipPositionTimer.current) {
    clearTimeout(updateTooltipPositionTimer.current)
    updateTooltipPositionTimer.current = undefined
}
updateTooltipPositionTimer.current = setTimeout(() => {
    updateTooltipPositionTimer.current = undefined
    forceUpdate()
}, 0)

...

useEffect(() => {
    return () => {
        if (updateTooltipPositionTimer.current) {
            clearTimeout(updateTooltipPositionTimer.current)
            updateTooltipPositionTimer.current = undefined
        }
    }
}, [])