So i have this react functional component:
export default function Login() {
const [loginData, setLoginData] = useState<Credentials>({ email: "", password: "" })
useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
return () =>
document.removeEventListener('keydown', handleKeyDown)
}, [])
function handleKeyDown(event: any) {
if (event.key === "Enter")
confirm()
}
function confirm() {
httpPut<Credentials, void>("user/login", loginData).then(
res => {
var authorization = res.headers.authorization
const parsed = jwt_decode(authorization)
console.log(parsed)
},
err => {
console.log(err)
}
)
}
return ...
}
I have encountered some weird behaviour, at least to my knolwedge. The useEffect where im adding the event listener for the enter key, is saving the state of loginData. By this I mean that if I fill the inputs and click the button, all works fine, but when i click the enter key, (i put a console.log in my axios requests so it logs the data) it comes { email: "", password: "" }.
Now, by trying arround I managed to fix it by adding the loginData as a dependency of the useEffect like this:
useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
return () =>
document.removeEventListener('keydown', handleKeyDown)
}, [loginData])
But should this be happening? It's not my first time with React and I've always added the listeners in the useEffect with no dependencies. To me this seems strange that it is kinda "saving" the state of loginData in the event listener itself. Any idea why this happens? Or even if it's supposed to? I got it to work but i'd like to understand why.
CodePudding user response:
You're seeing a stale closure (a story as old as React hooks).
- Once the component mounts, the effect is run. Since it has an empty dependency array, it's not run again.
- The effect function closes over (captures), shall we say, version 1 of
handleKeyDown. - Version 1 of
handleKeyDowncloses over version 1 ofconfirm. - Version 1 of
confirmcloses over the initial state ofloginData. (Since you're supposed to replace state atoms such as objects or arrays instead of mutating them internally so React can see the changes, the original object remains closed-over). - When the state atom
loginDatachanges, new versions ofconfirmandhandleKeyDownare created (in fact, whenever the component updates, new versions are created, but that's beyond the point for this discussion), but the event handler from step 1/2 is still bound to version 1. This is why that event handler sees the original version ofloginDataand why settingloginDataas a dependency fixes things (since it causes new versions ofhandleKeyDownandconfirmto be used for the event).
Your options are, more or less:
- making
loginDataa dependency of the effect, as you did. This will cause extraneous event handler unsubscribe/subscribe cycles, but those are likely not a performance issue. - making the whole chain of functions
useCallbacked, so they're only updated as necessary. This is more or less the above, though. - using a ref to "box" the latest state of
loginData(e.g. theuseLatesthook), and using that boxed ref in yourconfirmfunction, i.e.loginDataRef.current. Sinceconfirmonly closes over the ref (which is a "box" containing a mutable reference) and not overloginDataRef.current, accessingcurrentwould always get the latest state. - like above, using a ref to box the latest version of
confirm, with a "trampoline" call toconfirmRef.current()instead of some closed-overconfirm.
