DEV Community

Phil Dibowitz
Phil Dibowitz

Posted on

exhaustive-deps false positives or my bug?

[This is also posted as a linter Issue 25205 which follows up from 14920 which is referenced below - but I'm guessing I actually have the bug, and so I'm asking here]

I have a Component, StatusBox. All it does is pop up a little overlay to tell you that, for example, something was submitted. If there's a timeout, after a timeout, it fades away, otherwise it stays until someone clicks on it.

As it stands my code now looks like:

let innerTimeout
let outerTimeout

 * A box for appearing status messages
const StatusBox = ({content, setContent, timeout, type}) => {
    const div = React.useRef(null)
    const [opacity, setOpacity] = React.useState(0)
    const [className, setClassName] = React.useState(type)

    const handleClick = e => {

    const clear = React.useCallback((e) => {
        console.debug("StatusBox: Clearing opacity, currently:", opacity)
         * If we remove the class now, it'll cut short the fade-out.
         * If we remove the text now, it'll disappear abruptly before the
         * fade-out.
         * HOWEVER, if we DON'T clear the text, then the overlay is still
         * there, blocking the page (in the case of a bigger status), so
         * we need to clear it after the transition. So get the timing of
         * that, and add 50ms just to be safe. And while we're at it,
         * we can clear the classes...
        let t = window.getComputedStyle(div.current).transitionDuration
        console.debug("StatusBox: computed transition", t)
        // that's something like 0.4s, we need to drop the 's', convert
        // to a number, and the turn into milliseconds. Trying to use a
        // string in a math equation causes JS to cast it for you
        t = t.replace('s', '') * 1000
        t += 50
        console.debug("StatusBox: Setting timeout to clear type/content in", t)
        innerTimeout = window.setTimeout(() => {
        }, t)
    }, [setContent, opacity])

    React.useEffect(() => {
            `StatusBox: in useEffect, timeout ${timeout}`
        // If we're setting content to null, we've already set the opacity
        // so we can return null to not re-render
        if (content == null) {
            return null

        // otherwise, we need to appear!
        console.debug("StatusBox: setting opacity to 1")

        // if we have no timeout, we're done
        if (timeout == null) {

        // otherwise, set the timeout
        console.debug('StatusBox: setting timeout for', timeout)
        // nuke any timeout already there
        if (innerTimeout) {
            console.debug("clearing inner:", innerTimeout)
        if (outerTimeout) {
            console.debug("clearing outer:", innerTimeout)
        outerTimeout = window.setTimeout(clear, timeout)
    }, [content])

    return <div
Enter fullscreen mode Exit fullscreen mode

This incorporates a suggestion in 14920 to move clear to be a useCallback. However, the linter wants both opacity and setContent in the deps list of the useCallback, and then content, clear, and timeout, in the deps list of useEffect. That leads to an infinite loop in the clearing of the box and it never clears.

I think @keanemind, in that bug, was trying to address that with:

To fix the stale clear problem, you can put clear into the dependencies array and then have your effect clean up by cancelling the outerTimeout. Now, when clear changes, the timeout with the stale clear will be cancelled, and then the effect will run again. And by using useCallback on clear, you can have clear change only when setContent changes.

But I didn't follow that. I... do clear outerTimeout in the useEffect already. I tried moving the clear to the very top of the useEffect in case they meant I might not clear it in some cases, but that didn't fix it.

@keanemind also asked:

By the way, is there a reason you don't unmount the overlay after the animation is over, rather than setting the content to null? That part is a little odd to me.

I briefly used class components when I was learning React, but quickly found that the recommendation was to use function components, and in function components I ... don't know how to unmount things. That seems to be only a thing in class components.

But also, if I unmounted it, wouldn't that <StatusBox> not be there the next time setContent was called?

I'm fairly new to both JS and React, so apologies in advance if I'm missing obvious stuff, and thanks!

Oldest comments (0)