I have a queryTerm
variable as a dependency to a useEffect
hook. I also register a handleKeyPress
event listener in the same hook.
I am facing an issue whereby when the queryTerm
dependency changes, the queryTerm
value in the handleKeyPress
listener is always one step behind the new queryTerm
value.
How would I go about synchronising the two?
import React, { useState, useEffect } from 'react'
const Test = () => {
const [queryTerm, setQueryTerm] = useState('')
const handleChange = (event) => {
setQueryTerm(event.target.value)
}
useEffect(() => {
console.log({ outside: queryTerm })
const handleKeyPress = (event) => console.log({ inside: queryTerm })
window.addEventListener('keydown', handleKeyPress)
return () => window.removeEventListener('keydown', handleKeyPress)
}, [queryTerm])
return (
<div className='App'>
<input onChange={handleChange} />
</div>
)
}
export default Test
CodePudding user response:
There are actually 3 issues here:
- The
"keydown"
and"change"
events occur in a certain order, so if you log something during the 1st, it will surely not have been affected yet by the 2nd - Even if you change the state and log it within the same event listener, the log will not be affected by the state change yet, because the latter will be effective on next render only; see The useState set method is not reflecting a change immediately
- When building a callback that uses some state in its body, it actually "embeds" a stale value of that state as its closure; think about the state variable as being a different reference every render (the previous link also explains this); workarounds include leveraging the
useRef
hook, but it depends on each specific situation
In your case, it is unclear why you need to use your state on "keydown"
(and why it must be attached to window
rather than to the <input/>
), and expect it to reflect a state change that occurs on "change"
event?
It might be a contrived example, but as is, you could simply do:
const handleChange = (event) => {
setQueryTerm(event.target.value)
// 1. Do everything in the same event handler
// 2. and 3. Use directly the new value
// instead of reading the state that will
// change only on next render
console.log({ inside: event.target.value })
}
CodePudding user response:
(This is effectively the same thing ghybs said, but i wandered off without hitting submit after typing it up; posting anyway in the hope that it still might provide some value.)
The keydown
event fires before the input's onChange
. So when you type a character into the input, the following occurs in this order:
handleKeyPress
fires, logging the current (about-to-be-updated)queryTerm
value. (inside)handleChange
fires, updatingqueryTerm
with the new value.- Your effect runs, logging the new value (outside), and attaching a new
handleKeyPress
with the new value.
CodePudding user response:
Let's go step by step what happens in your component
useEffect
has this[queryTerm]
dependency. That means in initial render and each timequeryTerm
changes the code insideuseEffect
will run. So in the initial render, this will run firstconsole.log({ outside: queryTerm })
you will see outside: ""
. also you will register handleKeyPress
for keydown
event. this will not run, it will be just registered.
write "a" inside the input element. Since we registered an event handler for
keydown
event and this event handler is synchronous it will run so you will see the current state// meanwhiwle async setState is updating the state inside:""
Because setState is async code, it will run after. When setState
finishes its execution, queryTerm
will be set to "a". Since you have [queryTerm]
dependency, your component will rerender but before it rerenders, the return cleanup
function will be called so you will remove the event handler for keydown
. After the cleanup is finished, new useEffect
will run, console.log({ outside: queryTerm })
will be executed and you will see the updated state
outside:"a"
Since you removed the event handler for keydown
before second rerender, this event handler will NOT run. But you will be registering a new event handler for keydown.
now you add "l".
handleKeyPress
will run so you will see the current state on consoleinside:"a"
then async setState will run, state will change, before rerender, your clean up will be fired, then your component will rerender and console.log({ outside: queryTerm });
will be fired so you will see the updated the state
outside:"al"