Home > Net >  Why does react event listener is saving the state?
Why does react event listener is saving the state?

Time:04-01

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).

  1. Once the component mounts, the effect is run. Since it has an empty dependency array, it's not run again.
  2. The effect function closes over (captures), shall we say, version 1 of handleKeyDown.
  3. Version 1 of handleKeyDown closes over version 1 of confirm.
  4. Version 1 of confirm closes over the initial state of loginData. (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).
  5. When the state atom loginData changes, new versions of confirm and handleKeyDown 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 of loginData and why setting loginData as a dependency fixes things (since it causes new versions of handleKeyDown and confirm 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 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. the useLatest hook), and using that boxed ref in your confirm function, i.e. loginDataRef.current. Since confirm only closes over the ref (which is a "box" containing a mutable reference) and not over loginDataRef.current, accessing current would always get the latest state.
  • like above, using a ref to box the latest version of confirm, with a "trampoline" call to confirmRef.current() instead of some closed-over confirm.
  • Related