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
handleKeyDown
closes over version 1 ofconfirm
. - Version 1 of
confirm
closes 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
loginData
changes, new versions ofconfirm
andhandleKeyDown
are 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 ofloginData
and why settingloginData
as a dependency fixes things (since it causes new versions ofhandleKeyDown
andconfirm
to be used for the event).
Your options are, more or less:
- making
loginData
a 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
useCallback
ed, 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. theuseLatest
hook), and using that boxed ref in yourconfirm
function, i.e.loginDataRef.current
. Sinceconfirm
only closes over the ref (which is a "box" containing a mutable reference) and not overloginDataRef.current
, accessingcurrent
would 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
.