Почему в контролируемом input прыгает каретка в React?

Вопрос: почему в контролируемом input прыгает каретка в React?

Линия поддержки
Линия поддержки Ответы на вопросы пользователей
30 октября 2025

Рассказали, в чем причина и как решить проблему, когда курсор в React-компоненте прыгает в конец или меняет свое место при вводе текста.

Изображение записи

Комментарий пользователя

Здравствуйте! При работе с контролируемым input в React курсор (каретка) постоянно прыгает в конец или в другое место. Почему так происходит и как этого избежать?

Новоселова Олеся Пользователь

Ответ специалиста

Здравствуйте, Олеся! Причина в том, что при контролируемом вводе вы заменяете value поля — React заново рендерит компонент и DOM-элемент получает новое значение.Если при этом ваше форматирование меняет строку (добавляет или удаляет символы), курсор сбивается и прыгает не туда, где его ожидают увидеть.

Другими словами: React не знает, где именно должен быть курсор после замены value, поэтому он просто ставит его на конец, и пользователь видит «прыжки».

Решение — вычислять новую позицию курсора и восстановить ее сразу после обновления DOM. Для этого удобно хранить ссылку на input и желаемую позицию, а ставить selection с помощью setSelectionRange в useLayoutEffect (он выполняется синхронно после DOM-обновления, до перерисовки).

  • Полина Цуканова

    Полина Цуканова

    Фронтенд-разработчик

Функция форматирования (пример — телефон, оставляем только 10 цифр):


      function formatPhone(raw) {
  const digits = raw.replace(/\D/g, '').slice(0, 10);
  if (digits.length < = 3) return digits;
  if (digits.length < = 6) return `(${digits.slice(0,3)}) ${digits.slice(3)}`;
  return `(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}`;
}

Объявления состояния и рефов в компоненте:


      const [value, setValue] = useState('');
const inputRef = useRef(null);
const desiredPosRef = useRef(null);

desiredPosRef хранит позицию, которую мы хотим установить после обновления value. Обработчик onChange: считаем, сколько «полезных» символов слева от курсора, форматируем и сохраняем желаемую позицию:


      const onChange = (e) => {
  const raw = e.target.value;
  const cursor = e.target.selectionStart ?? raw.length;
  const digitsLeft = raw.slice(0, cursor).replace(/\D/g, '').length;

  const formatted = formatPhone(raw);
  setValue(formatted);

  let pos = 0, counted = 0;
  while (pos < formatted.length && counted < digitsLeft) {
    if (/\d/.test(formatted[pos])) counted++;
    pos++;
  }
  desiredPosRef.current = pos;
};

Мы сопоставляем не абсолютный индекс, а «сколько цифр слева», поэтому форматирование (скобки/пробелы) не ломает позицию.Восстановление курсора синхронно после применения value:


      useLayoutEffect(() => {
  const el = inputRef.current;
  const pos = desiredPosRef.current;
  if (el && typeof pos === 'number') {
    const safe = Math.min(pos, el.value.length);
    el.setSelectionRange(safe, safe);
    desiredPosRef.current = null;
  }
}, [value]);

useLayoutEffect гарантирует, что установка selection произойдет сразу после DOM-обновления и до отрисовки кадра — пользователь не увидит мерцания.

Разметка (в компоненте):


      <input ref={inputRef} value={value} onChange={onChange} inputMode="tel" placeholder="Enter phone" />

Контролируемое поле с ref — все, что нужно для управления курсором.
А еще больше полезных приемов — в рассылке Академии Selectel.