Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
GRAMMAC1 opened this issue Jul 9, 2020 · 4 comments
Labels
Component: DOM Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug Type: Bug

Comments

@GRAMMAC1
Copy link

GRAMMAC1 commented Jul 9, 2020

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

@GRAMMAC1 GRAMMAC1 added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Jul 9, 2020
@bvaughn
Copy link
Contributor

bvaughn commented Jul 9, 2020

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

@bvaughn bvaughn changed the title Bug: Trigger effect on the onKeyDown will cause input loss Bug: Unrelated state update in onKeyDown blocks onChange and breaks controlled component Jul 9, 2020
@tsybanov
Copy link

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.

@CalvinLeGassick
Copy link

CalvinLeGassick commented Jul 19, 2020

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?

@gaearon
Copy link
Collaborator

gaearon commented Feb 10, 2021

This appears fixed in React 17.
https://codesandbox.io/s/samc-focus-demo-forked-u9qm8?file=/package.json

@gaearon gaearon closed this as completed Feb 10, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Component: DOM Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug Type: Bug
Projects
None yet
Development

No branches or pull requests

5 participants