skip to Main Content

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


  1. Chosen as BEST ANSWER

    After some debugging, I found that

    When setState is called, React will schedule a micro task to rerender.

    The assumption is wrong.

    For more details, check https://github.com/facebook/react/issues/31182#issuecomment-2421205201


  2. 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.

        const input = e.target;
        const start = input.selectionStart;
        const end = input.selectionEnd;
        input.value = input.value?.toUpperCase();
        input.setSelectionRange(start, end);
    

    in your case, perhaps this can be applied like so:

    const input = e.target
    const v = input.value;
    const start = input.selectionStart;
    const end = input.selectionEnd;
    
    queueMicrotask(() => {
      setValue2(v);
      input.setSelectionRange(start, end);
    }
    

    i hope that is helpful. cheers!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search