React controlled input cursor jumps

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;

Leave a Comment