React: Bug: Unrelated state update in onKeyDown blocks onChange and breaks controlled component

Created on 9 Jul 2020  ·  3Comments  ·  Source: facebook/react

Hi, I use Input controlled mode and subscribe to onKeyDown and onChange event. It will trigger an effect when user enter space. However, the space don't display on screen. Is this a bug on React or is there something wrong with the way I'm using it. But subscribe to onKeyUp is normal. This is confused.

React version: 16.13.1 and old

Steps To Reproduce

  1. enter space
  2. input element don't show space on the screen

Link to code example:

import React, { useState,useEffect } from 'react'

const App = () => {
  const [count, setCount] = useState(0)
  const [value, setValue] = useState('')
  const [code, setCode] = useState('add')

  useEffect(() => {
    if (code === 'add') {
      setCount(c => c + 1)
    } else {
      setCount(c => c - 1)
    }
  }, [code, setCount])

  const keyDown = e => {
    if (e.keyCode === 32) {
      console.log('space~')
      if (code === 'add') {
        setCode('minus')
      } else {
        setCode('add')
      }
    }
  }

  const handleChange = e => {
    setValue(e.target.value)
  }

  return (
    <div>
      <div>{count}</div>
      <br />
      <input onKeyDown={keyDown} value={value} onChange={handleChange} />
    </div>
  )
}

export default App

The current behavior

input element can't receive the space

The expected behavior

input element can receive the space

DOM Unconfirmed Bug

All 3 comments

It looks like the state update in the onKeyDown handler interrupts the event and prevents the onChange handler from being called. Because controlled components show only what you tell them to (the value state in your example) this results in the space character never appearing.

This looks like a bug to me in the way these two events interplay.

cc @trueadm @gaearon for their event system expertise

Hi, I'm new here, trying to learn React architecture.

@bvaughn Here is something that I found about the problem.

onKeyDown event doesn't block onChange rather that it triggers earlier than input is modified. Basically, onKeyDown goes earlier than onChange because text input happens in onKeyUp.

In this particular case, the problem is that within keyDown, when setCode calls, on the first dispatchAction _Fiber_ has memoizedState without the new character (obviously) and e which is going to be added. In the end, it adds the new character to the input. Then useEffect() triggers dispatchAction again within the _same queue_ (not sure if it's queue) but it doesn't know anything about updated props. As a result, when it commitRoot, it restores pendingProps which are the same as they were in memoizedState. In short, the character (space in this example) is added and then immediately old state restores without the new character (space), so it looks like it doesn't add anything.

As an example of not using _same queue_ (?), this particular example:

useEffect(() => {
    if (code === 'add') {
      setCount(c => c + 1)
    } else {
      setCount(c => c - 1)
    }
  }, [code, setCount])

could be modified to:

useEffect(() => {
    if (code === 'add') {
      setTimeout(() => setCount(c => c + 1), 0)
    } else {
      setTimeout(() => setCount(c => c - 1), 0)
    }
  }, [code, setCount])

and it will work.

I hope it makes sense.

Ran into this same issue, wrapping my setState call in a a setTimeout in the keydown handler fixed it.

I'm curious to know if this is something that should work differently under the hood in React or if this error points to a bigger mistake or bad pattern on the developer side?

Was this page helpful?
0 / 5 - 0 ratings