Exponential re-rendering with eventListener on state update

Viewed 197

I'm trying to use an eventListener on "keydown" that'll update my state (a boolean named showMenu). I've placed it on the useEffect, but it does not work properly and I can't figure out why... It re-renders the component exponentially if I put showMenu on the array at the end of my useEffect. If I don't put showMenu at the end of my useEffect, the showMenu will only be updated once (from false to true)

Here is the code that re-renders exponentially

const [showMenu, setShowMenu] = useState(false);


useEffect(() => {
    window.addEventListener("keydown", (e) => handleMenu(e));
    return () => {
        window.removeEventListener("keydown", (e) => handleMenu(e));
    };
}, [showMenu]);

const handleMenu = (e) => {
    if (e.key == "m") {
        setShowMenu(!showMenu);
    }
};
2 Answers

removeEventListener takes a function reference but you are creating an anonymous function each time:

useEffect(() => {
    window.addEventListener("keydown", handleMenu);
    return () => {
        window.removeEventListener("keydown", handleMenu);
    };
}, [showMenu]);

By removing the anonymous function wrapper (aka eta reduction) addEventListener and removeEventListener get the same function reference.

Alternatively if you only support modern browsers and want to ignore the concept of function references addEventListener supports AbortSignals:

useEffect(() => {
    const ac = new AbortController();
    window.addEventListener("keydown", (e) => handleMenu(e), { signal: ac.signal });
    return () => ac.abort();
}, [showMenu]);

As a side note there is really no reason to pass showMenu into the dependency list of the function and I'd not pass it.

OK just figured it out. The useEffect method was not the problem. The logic is good. The issue was with the (e) => handleMenu(e) that I need to change to handleMenu directly. See below:

const [showMenu, setShowMenu] = useState(false);

const handleMenu = (e) => {
    if (e.key == "m") {
        setShowMenu(!showMenu);
    }
};

useEffect(() => {
    window.addEventListener("keydown", handleMenu);
    return () => {
        window.removeEventListener("keydown", handleMenu);
    };
}, [showMenu]);

Kudos to Benjamin for giving me the answer : I needed to pass my handleMenu function reference instead of using (e) => syntax which creates a new function each time based on my handleMenu function, it explains the exponential rendering :)

Related