Skip to content

Why does addEventListener and removeEventListener inside useEffect() require an arrow function?

I’m trying to set onTop state to true if the user has scrolled to the top and false otherwise. I tried the following.

function Test()
{
    const [ onTop, setOnTop ] = useState( true )

    const watchScroll = () =>
    {
        if ( window.scrollY < 100 ) setOnTop( true )
        else setOnTop( false )
    }

    useEffect(() => {
        window.addEventListener(`scroll`, watchScroll )
        return  window.removeEventListener(`scroll`, watchScroll )
    }, [ watchScroll ])

    return (

        <div>{ onTop ? `On Top` : `Not On top` }</div>

    )
}

The above example doesn’t work but throws no error either.

function Test()
{
    const [ onTop, setOnTop ] = useState( true )

    const watchScroll = () =>
    {
        if ( window.scrollY < 100 ) setOnTop( true )
        else setOnTop( false )
    }

    useEffect(() => {
        window.addEventListener(`scroll`, () => watchScroll() )
        return  window.removeEventListener(`scroll`, () => watchScroll() )
    }, [ watchScroll ])

    return (

        <div>{ onTop ? `On Top` : `Not On top` }</div>

    )
}

Note that I added an arrow and braces to the second parameter function. The above example works as intended. Can anyone explain why? Thanks very much!

Answer

The reason your initial function doesn’t work well is because on every re-render a new instance of the function is created and thee previous one is removed since you are passing the function as a dependency to useEffect. Also since you are not executing the window.removeEventListener in a cleanup function, it runs immediately causing the listener to be removed immediately.

You can solve it either by

function Test()
{
    const [ onTop, setOnTop ] = useState( true )

    const watchScroll = useCallback(() =>
    {
        if ( window.scrollY < 100 ) setOnTop( true )
        else setOnTop( false )
    }, []);

    useEffect(() => {
        window.addEventListener(`scroll`, watchScroll )
        return () => window.removeEventListener(`scroll`, watchScroll )
    }, [ watchScroll ])

    return (

        <div>{ onTop ? `On Top` : `Not On top` }</div>

    )
}

or

function Test()
{
    const [ onTop, setOnTop ] = useState( true )

   
    useEffect(() => {
        const watchScroll = () => {
           if ( window.scrollY < 100 ) setOnTop( true )
           else setOnTop( false )
       }

        window.addEventListener(`scroll`, watchScroll )
        return ()=> window.removeEventListener(`scroll`, watchScroll )
    }, [ ])

    return (

        <div>{ onTop ? `On Top` : `Not On top` }</div>

    )
}

Also note that with arrow functions, you solution works because to removeEventListener you need to pass the same function reference for it to work properly, if you use arrow function, the listener doesn’t cleanup up and hence your implementation works