Home > Enterprise >  ReactJS unexpected behavior in window event
ReactJS unexpected behavior in window event

Time:03-23

Stuck on an issue that is probable easy to fix. I have the following React code (which can be found at CodeSandbox):

export const App = ()=> {
  const [toggle,setToggle] = useState(false);

  const setItemToggle = (e) => {
    e.stopPropagation()
    e.target.tagName==="H1" ? setToggle(!toggle) : setToggle(false)
    e.preventDefault()
  }

  useEffect(()=>{
    window.addEventListener("click",setItemToggle,false);
    return () => window.removeEventListener("click",setItemToggle,false);
  },[])


  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Current Value Of toggle is {toggle ? "True" : "False"}</h2>
    </div>
  );
}

When you click on the <h1>Hello CodeSandbox</h> I expected the toggle to change accordingly; however, it changes once and doesn't toggle after that. Clicking outside of the element does change the toggle to false. I know it has to do with the eventListener added to the Window but I can't track it down. Thanks for any help.

CodePudding user response:

You have a stale prop problem

  • since the event handler you assign to the event listener is using the initial closure it was created in (so it doesn't see changes to the closed-over toggle value),
  • and you don't set the function as a dependency to the useEffect invocation so a new version of the function never gets used.
    Had you omitted the empty dependency array altogether, this would have worked, with the caveat that you would be continuously re-registering event handlers.

The easy way to fix this is to not close over (refer to) the toggle variable at all in the event handler at all by using the function form of setState: setToggle(toggle => !toggle).

CodePudding user response:

You are simply changing the state value by doing setState(!state). This pattern does not guarantee that you have the correct value of state. You are better off using the functional pattern:

import { useEffect, useState } from "react";
import "./styles.css";

const App = () => {
  const [toggle, setToggle] = useState(false);

  const setItemToggle = (e) => {
    e.stopPropagation();
    e.target.tagName === "H1"
      ? setToggle((toggle) => !toggle)
      : setToggle(false);
    e.preventDefault();
  };

  useEffect(() => {
    window.addEventListener("click", setItemToggle, false);
    return () => window.removeEventListener("click", setItemToggle, false);
  }, []);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Current Value Of toggle is {toggle ? "True" : "False"}</h2>
    </div>
  );
};

export default App;

setState(prevState => !prevState) is a sure way to ensure you have the correct previous value of state. This is the ideal way to set state when new value of state depends on previous value of state.

Sandbox Link

The problem is that your function is unaware of the new value of state, one more way to tackle is using the dependancy array. As the name suggests you are defining the dependencies of the your code in the array. Just pass it as the second param to useEffect and you should be good to go:

import { useEffect, useState } from "react";
import "./styles.css";

const App = () => {
  const [toggle, setToggle] = useState(false);

  useEffect(() => {
    const setItemToggle = (e) => {
      e.stopPropagation();
      e.target.tagName === "H1" ? setToggle(!toggle) : setToggle(false);
      e.preventDefault();
    };

    window.addEventListener("click", setItemToggle, false);
    return () => window.removeEventListener("click", setItemToggle, false);
  }, [toggle]);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Current Value Of toggle is {toggle ? "True" : "False"}</h2>
    </div>
  );
};

export default App;

We are once again tackling the problem of incorrect state value but this time by rerunning the code inside the useEffect callback and hence redefining the function setItemToggle.

Link

Note: Also if a function is only relevant inside useEffect, best to define it there only. This is also one of the pros of using hooks instead of classes. Your relevant code is in one place.

CodePudding user response:

You can just add onClick to h1 element and pass a function directly there.

<h1 onClick={() => setToggle(!toggle)}>Hello CodeSandbox</h1>

some good resource on the topic of Handling Events in react can be found here and working sandbox of what I present can be found here

  • Related