Комментарий пользователя
Здравствуйте! При работе с контролируемым 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.