facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
228.58k stars 46.79k forks source link

Bug: Controlled state input weird behavior when state update is postponed to the next micro task #31182

Open yf-yang opened 1 week ago

yf-yang commented 1 week ago

React version: v18.3

Steps To Reproduce

  1. Open the codesandbox link below
  2. Add characters in the middle of each input
  3. The first input works as expected. In the second input, the cursor will instantly jump to the end, also input methods will not work.

Link to code example: https://codesandbox.io/p/sandbox/74mhkd

The current behavior

By adding an additional queueMicrotask to the second input, two inputs behavior are different.

https://github.com/user-attachments/assets/5cc7e2f6-0706-43ef-88cd-08177ab59a35

The expected behavior

Work loop stuff should not affect DOM manipulation.

Is it a bug or feature? Is it a react stuff?

adi-ydv-1 commented 5 days ago

I have run it but haven't encountered any issues with it.

yf-yang commented 5 days ago

Investigation

Generally speaking, React 18 auto batching's mechanism of event handlers is like:

function domOnValueChange(e) {
  reactComponentOnChange(e); // collect state updates but does not actually performs those updates, not an actual function name
  performStateUpdates(); // here the auto batching works, not an actual function name

  // some additional codes to maintain DOM node states
  restoreControlledState(element, props); // actual function, use latest props value of the corresponding component to update element  
}

The restoreControlledState function will then deal with <input />, <textarea /> and <select /> specifically. It will finally call three different functions called updateWrapper that handles the DOM node.

Suppose this is an <input /> component, then when a character is typed, the DOM <input /> node will change, its value will add one character, and triggers the change event handler. After some state changes, it will finally call updateWrapper.

Here, if the state is updated in the same micro task, then the props.value property will be the same with current DOM node, so the cursor location will not be affected.

Instead, if the state update is postponed to another micro task, then props.value remains unchanged (suppose it is 'second'), but the DOM node's value has already added one more character (suppose it is 'secaond'). Here React's updateWrapper will reset the DOM node's value to the unchanged one ('secaond' -> 'second'). During the reset operation, the cursor location is lost. During the next micro task, although the state will finally be updated to the one with one more character ('secaond'), the cursor location is lost and would be moved to the last character.

Thoughts

I personally think this issue should be categorized as a wont fix, but maybe someone can offer a solution to make it more sound, I'm not sure.

NOTE: In latest React main branch some function name has changed, but their names are pretty similar and it is not difficult to find the corresponding logic.