question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Bug: cursor jumps to end of controlled <input> tag when value is modified

See original GitHub issue

React version: 16.13.1

Steps To Reproduce

  1. Make an <input> tag controlled, by setting its value in response to onChange
  2. Apply a transformation to the value (for example, replace spaces with underscores)
  3. Move cursor to the middle of the text and edit it

Link to code example:

https://gist.github.com/iain-merrick-fanduel/b9cea57baa9f20a5d288a0fcd6e7ee5e

Adapted from CodePen example (https://codepen.io/gaearon/pen/VmmPgp?editors=0010) on https://reactjs.org/docs/forms.html

The current behavior

If the transformation changes the value, the cursor is moved to the end of the input.

The expected behavior

Cursor should remain at the original position if possible (this is the behaviour of the TextInput component in React Native).

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:1
  • Comments:10 (1 by maintainers)

github_iconTop GitHub Comments

4reactions
vkurchatkincommented, Mar 27, 2020

But React doesn’t provide that low-level access (by design).

Well, that’s where you are wrong. It’s not that hard to implement this in React. Here is a demo: https://codesandbox.io/s/boring-dirac-utq82

1reaction
cefncommented, Aug 8, 2022

React can’t know where to put the selection after you’ve modified it

I don’t agree with @gaearon for a common case. Possibly this is what people were complaining about in https://github.com/facebook/react/issues/955 or https://github.com/facebook/react/issues/5386

UPDATE: After re-reading this thread the OPs case is different from https://github.com/facebook/react/issues/955 and https://github.com/facebook/react/issues/5386 so this comment is misplaced. Happy to move this to any open issue which is better aligned or open a new one. I’ve left the original comment below for context…

Where an edit in the control caused the state change, the contents of the control are suited to preserving the selection. The value held in the control is already the value react is attempting to set. It originated in the control and may be taking the long way round owing to some async state-propagation mechanism. For this case, preserving the selection is a well-defined behaviour, and the ‘loss’ of selection may be considered an artefact of the recommended ‘controlled’ component binding approach combined with current react reconciliation behaviour.

One of two alternative strategies seem reasonable for reconciling an already-identical DOM value: a) Don’t set the value at all, the selection (cursor) will be implicitly preserved. b) DO restore the selection (cursor) after redundantly setting the value and therefore ‘breaking’ the selection. This is guaranteed to be a well-defined behaviour (and probably what was intended).

This assumes that state changes originated from a controlled component can’t ‘back up’. If a queue of changes sends a value from three keypresses ago, then two keypresses ago, then one keypress ago it will encounter a the dom value which is from the future, and can’t copy across the selection meaningfully.

I’ve shared the workaround below for discussion. It embodies selection-preserving strategy b) in the form of a hook. Doing it with strategy a) would involve monkey patching reconciliation internals.

The workaround was effective for my async state engine in simple interactive testing. Without it, every edit caused a loss of cursor position, even though it was reconciling to a value already in the DOM. I’d be curious if the approach works for others.

It can be used like…

<input type="text" {...useBinding(label)} />
import { useLayoutEffect, useRef } from "react";

function recordPriorDom({
  current,
}: React.RefObject<HTMLInputElement | HTMLTextAreaElement>) {
  if (current === null) {
    return null;
  }
  const { value, selectionStart, selectionEnd } = current;
  return {
    value,
    selectionStart,
    selectionEnd,
  };
}

export function useBinding<E extends HTMLInputElement | HTMLTextAreaElement>(
  value?: string
) {
  // ref for the (eventually) bound DOM element
  const ref = useRef<E>(null);

  // record DOM before change
  const prior = recordPriorDom(ref);

  // consider restoring selection after DOM change
  useLayoutEffect(() => {
    if (prior && prior.value === value) {
      // DOM value was already aligned.
      // Change probably originated from this control
      // Selection can be preserved
      const { current } = ref;
      if (current) {
        current.selectionStart = prior.selectionStart;
        current.selectionEnd = prior.selectionEnd;
      }
    }
  }, [value]);

  // props for the bound control
  return {
    ref,
    value,
  };
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

React controlled input cursor jumps - Stack Overflow
If your value is controlled by state, React will maintain the input's cursor position. The problem is when the input receives a change...
Read more >
Solving Caret Jumping in React Inputs - DEV Community ‍ ‍
When you inject programmatically a different value in a DOM input, the input makes no assumption about caret position and moves it to...
Read more >
The Curious Case of Cursor Jumping - Mutually Human
Cursor jumping is simply when the cursor jumps from one location to another, unexpectedly, while the user is typing into a text field....
Read more >
React: why is my cursor jumping to the end of the input field ...
React: why is my cursor jumping to the end of the input field after the input is modified. It works ok but when...
Read more >
Form Input | Components - BootstrapVue
The formatter must return the value as a string. Note: With non-lazy formatting, if the cursor is not at the end of the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found