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.
You have a stale prop problem
toggle value),useEffect invocation so a new version of the function never gets used.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).
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.
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.
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.