Here's a drop-in replacement for the <input/>
tag. It's a simple functional component that uses hooks to preserve and restore the cursor position:
import React, { useEffect, useRef, useState } from 'react';
const ControlledInput = (props) => {
const { value, onChange, ...rest } = props;
const [cursor, setCursor] = useState(null);
const ref = useRef(null);
useEffect(() => {
const input = ref.current;
if (input) input.setSelectionRange(cursor, cursor);
}, [ref, cursor, value]);
const handleChange = (e) => {
setCursor(e.target.selectionStart);
onChange && onChange(e);
};
return <input ref={ref} value={value} onChange={handleChange} {...rest} />;
};
export default ControlledInput;
...or with TypeScript if you prefer:
import React, { useEffect, useRef, useState } from 'react';
type InputProps = React.ComponentProps<'input'>;
const ControlledInput: React.FC<InputProps> = (props) => {
const { value, onChange, ...rest } = props;
const [cursor, setCursor] = useState<number | null>(null);
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
ref.current?.setSelectionRange(cursor, cursor);
}, [ref, cursor, value]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCursor(e.target.selectionStart);
onChange?.(e);
};
return <input ref={ref} value={value} onChange={handleChange} {...rest} />;
};
export default ControlledInput;