Home > Blockchain >  Value in Event Listener attached through useEffect is always one step behind React State
Value in Event Listener attached through useEffect is always one step behind React State

Time:12-21

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:

  1. 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
  2. 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
  3. 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:

  1. handleKeyPress fires, logging the current (about-to-be-updated) queryTerm value. (inside)
  2. handleChange fires, updating queryTerm with the new value.
  3. 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 time queryTerm changes the code inside useEffect will run. So in the initial render, this will run first

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

    inside:"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"
  • Related