javascriptreactjsreact-functional-component

React state value is wrong in functional component when read from event handler


I am trying to convert a fairly simple React class component to functional component. Here on document click I want to toggle the isShown boolean value between true and false. But each time onClick is fired, the value isShown is always false, even though it was previously toggled to true. I do not understand what is going on here and how to fix it?

import React, { useEffect, useState } from 'react';

export default function Popover({}: any) {

    const [isShown, setIsShown] = useState(false);

    useEffect(() => {
        document!.addEventListener('click', onClick);
    }, []);

    function onClick(e: Event) {
        console.log(isShown);
        setIsShown(!isShown);
    }

    return (<div></div>);
}

Solution

  • Let's focus on the useEffect. In there you added a listener that the callback function is onClick. The onClick function is declared with isShown: false at the first. So the onClick will be something like that:

        function onClick(e: Event) {
            console.log(false); // isShown is `false` and will be logged
            setIsShown(!false); // isShown will be set to `true`
        }
    

    So when you click on the button, the isShown value will be changed to true but the onClick function won't be declared again for the listener in the useEffect and the callback function of the event listener is the one I explained before. Now, you know the problem and you have two solutions:

    1. Changing the onClick funtion to not to depend on the isShown state like this:
        function onClick(e: Event) {
            setIsShown(previousValue => !previousValue);
        }
    
    1. Changing the useEffect to be sensetive to the onClick changes. For this solution, to avoid the infinit rerenders, you need to memoize the onClick function or move the function in the body of the useEffect:
        const onClick = useCallback((e: Event) => {
            console.log(isShown);
            setIsShown(!isShown);
        }, [isShown]);
    
        useEffect(() => {
            document!.addEventListener('click', onClick);
            return () => document!.removeEventListener('click', onClick);
        }, [onClick]);
    

    OR

        useEffect(() => {
            function onClick(e: Event) {
                console.log(isShown);
                setIsShown(!isShown);
            }
            document!.addEventListener('click', onClick);
            return () => document!.removeEventListener('click', onClick);
        }, [isShown]);