React: Bug: Bubbled onFocus handler that triggers an update prevents onChange handler in createRoot

Created on 13 Apr 2020  路  4Comments  路  Source: facebook/react

React version: I think any experimental version.
Tested using:

  • 0.0.0-experimental-5faf377df and
  • 0.0.0-experimental-e5d06e34b at least.

Steps To Reproduce

  1. Open CodeSandbox: https://codesandbox.io/s/createroot-broken-focus-onchange-ysrut?file=/src/App.js
  2. Attempt to click on the label text for the checkbox in the createRoot section
  3. Notice that the checkbox won't check - onChange isn't called
  4. Attempt to click on the label text for the checkbox in the render section
  5. Notice that the checkbox will check - onChange is called

Link to code example: https://codesandbox.io/s/createroot-broken-focus-onchange-ysrut?file=/src/App.js

The current behavior

When rendering using createRoot:

  • onFocus is called
  • onChange is not called

When rendering using render:

  • onFocus is called
  • onChange is called

My coworker and I spent a bit of time debugging this, if we at all schedule the onFocus setState handler (using setTimeout, or requestAnimationFrame for example), then onChange is always called from the input.

e.g.

handleFocus = () => {
  setTimeout(() => this.setState({ isHovered: true }), 0)
  // or
  requestAnimationFrame(() => this.setState({ isHovered: true }))
}

Additionally, if the label is a sibling to the input:

return (
  <>
    <input id="id" onChange={} type="checkbox" checked={} />
    <label htmlFor="id" onFocus={}>
      label text
    </label>
  </>
)

then the onChange handler will be called as well (Note: onFocus won't be called in this case because there isn't a focusable element within it).

Also worth noting, re-implementing the checkbox component using hooks will still run into the same bug - no differences between using classes vs hooks.

The expected behavior

Calling setState in onFocus shouldn't prevent onChange on a nested input element to be called. Both onFocus and onChange should be called.

Concurrent Mode Bug

Most helpful comment

Thanks for the detailed bug report @hamlim. I verified the same behavior with the latest experimental release as well (0.0.0-experimental-e5d06e34b). You can temporarily get around this by flushing the state update in handleFocus synchronously using ReactDOM.flushSync

ReactDOM.flushSync(() => {
  this.setState({ isHovered: true })
})

All 4 comments

Thanks for the detailed bug report @hamlim. I verified the same behavior with the latest experimental release as well (0.0.0-experimental-e5d06e34b). You can temporarily get around this by flushing the state update in handleFocus synchronously using ReactDOM.flushSync

ReactDOM.flushSync(() => {
  this.setState({ isHovered: true })
})

Yea, that one looks very similar and I think it could be the same issue

Note that as a temporary workaround, this fixes it:

  handleFocus = (e) => {
    ReactDOM.flushSync(() => {
      this.setState({ isHovered: true });
    });
  }
Was this page helpful?
0 / 5 - 0 ratings

Related issues

shirakaba picture shirakaba  路  85Comments

gaearon picture gaearon  路  133Comments

gaearon picture gaearon  路  227Comments

wohali picture wohali  路  128Comments

gaearon picture gaearon  路  104Comments