-
Notifications
You must be signed in to change notification settings - Fork 47k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug: cursor jumps to end of controlled <input> tag when value is modified #18404
Comments
This is very similar to #14904, but doesn't look quite the same:
In case this is browser-specific, I'm using Chrome 80 on OS X 10.14.6. |
As far as I know, this is the intended usage, as the docs specifically give this example:
That example exhibits the same bug — you can't edit the text field in the middle. |
This has nothing to do with React, that's normal browser behaviour |
In hand-written HTML, you'd normally fix this by saving the cursor position before changing the text, and restoring it afterwards. But React doesn't provide that low-level access (by design). Maybe React could manage the cursor position automatically? This would bring parity with React Native, where the |
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 |
Oho, neat! I can probably use this, thanks! Is this something that could/should be added as default behavior for the Alternatively, the example code could be updated, but it seems like a lot of boilerplate for a simple task. |
It's actually a little strange that the example docs use this approach. I've read that since React uses If you modify your handleChange function to the following, it should work as expected. Event pooling allows reuse of the event and because the setState is asynchronous, there is no guarantee on when it will be called and what value it holds at that instance. |
I think it is a bit against React philosophy. React is about making DOM transformations declarative, not about changing how it works. If you need such behaviour, you can make your own
I agree, there probably should be a warning in the docs explaining that it's not as _ straightforward_ to modify input this way.
No, your modified code is strictly equivalent to original code. What you can't do is use the event object asynchronously, including |
Woah. This advice in docs seems pretty bad. I don't think this is supported in that sense. This behavior is expected (React can't know where to put the selection after you've modified it). I'll remove the misleading docs section. |
I don't agree with @gaearon for the common case that the react state was actually driven by the change in the UPDATE: After re-reading this thread the OPs case is different from the referenced issues in #955 and #5386 which do not transform the value like the OP's case for this issue. This comment is therefore misplaced. I've left the original comment below for context... ORIGINAL COMMENT >>> 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: 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 a) in the form of a hook. The workaround was effective for my async state engine in simple interactive testing. This avoids treating the It can be used like... <input type="text" {...useBinding(label)} /> import { useEffect, useRef } from "react";
export function useBinding(value: string) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
const current = ref.current;
if (current) {
if (current.value !== value) {
// handle case where value DIDN'T originate from this control
// e.g. should overwrite because state was set outside
current.value = value;
current.setSelectionRange(current.value.length - 1, null);
}
}
}, [ref, value]);
return {
ref,
value,
};
} |
The proposed solutions seem a bit convoluted and still have visual artifacts (such as the cursor jumping to the end and back over two frames). Copying the state into a synchronous variable to act as a cache for the async store seems more straightforward and general: function useCachedState<T>(propValue: T): [T, (value: T) => void] {
const [lastValue, setLastValue] = useState(propValue);
const [currValue, setCurrValue] = useState(propValue);
if (propValue !== lastValue) {
setLastValue(propValue);
setCurrValue(propValue);
}
return [
currValue,
(value: T) => {
setCurrValue(value);
},
];
}
function EditableItem({
id,
value,
}: {
id: string;
value: string;
}) {
const [cachedValue, setCachedValue] = useCachedState(value);
const onChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
setCachedValue(e.target.value);
return updateSomeAsyncStore(e.target.value);
};
return <input type="text" value={cachedValue} onChange={onChange} />;
} This will make the update synchronous to the component and keep the caret position in the input. If the async store replies back with something that doesn't match the optimistic synchronous update, the async store will override the synchronous state. This approach is also general and works not just for text inputs but also for draggables or anything else. Ideally, though, the "someAsyncStore" would have a reactive system that can update synchronously rather than pushing this concern out into components. |
React version: 16.13.1
Steps To Reproduce
<input>
tag controlled, by setting itsvalue
in response toonChange
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).The text was updated successfully, but these errors were encountered: