CSB link: https://codesandbox.io/p/sandbox/74mhkd
Observation
I create two similar inputs in the sandbox. The first one is the most common, and it works as expected.
const [value1, setValue1] = useState("first");
<input
value={value1}
onChange={(e) => {
setValue1(e.target.value);
}}
/>
Then, for the second one, an additional microtask is added whenever a change happens.
const [value2, setValue2] = useState("second");
<input
value={value2}
onChange={(e) => {
const v = e.target.value;
queueMicrotask(() => {
setValue2(v);
}
}}
/>
Now, when I put the cursor in the middle of the content, and then add a character or delete a character, the cursor jumps to the end of the content after the next render.
Thoughts/Questions
If I’m wrong, please feel free to point it out.
I think it is related to how React updates vanilla javascript DOM nodes and some DOM specs, even browser implementations.
AFAIK, when setState is called, React will schedule a micro task to rerender. Therefore, the microtask when the change event is fired is different from the microtask when React manipulates the <input />
dom node. Amazingly the browser could remember the correct cursor location.
If that’s right, then I cannot understand why by adding another microtask, the browser loses those memories. Why does it happen?
2
Answers
After some debugging, I found that
The assumption is wrong.
For more details, check https://github.com/facebook/react/issues/31182#issuecomment-2421205201
i’m sad to say i don’t know enough exactly why React causes that behavior, but i believe you are correct in thinking that React’s handling of the typical setState "microtask" is different than your own queueMicroTask.
i had a similar issue when trying to force an input to be uppercase. the solution was to manually track the cursor position during the onChange event.
in your case, perhaps this can be applied like so:
i hope that is helpful. cheers!