В React‑компоненте ниже ввод текста иногда теряет фокус при каждом вводе: function Text({value,onChange}){ return onChange(e.target.value)} /> } В каких ситуациях контролируемый компонент теряет фокус, как это отлаживать и какие подходы (ключи, memo, useRef, разделение состояния) помогут сохранить UX и доступность
Кратко — почему это происходит, как отлаживать и какие практики помогают сохранить UX и доступность. Причины потери фокуса - Компонент полностью размонтируется и монтируется заново (например, изменился `key`, или возвращается другая ветка JSX). При ремонте DOM‑элемент теряет фокус. - Элемент заменяется другим элементом (изменился тип/структура дерева) — React создаёт новый input. - Принудительная переинициализация value/значения (в том числе форматирование строки), из‑за которой курсор/выделение сбрасываются. - Переход между контролируемым и неконтролируемым режимом (value становится `undefined`/`null` или обратно) — React даёт предупреждение и поведение ненадёжно. - Частые переотрисовки родителя, передача новой (ненужной) ссылки пропов (функция/объект) — дочерний компонент заново рендерится/реинициализируется. - В DEV: React.StrictMode может двойной рендер в разработке (но это не должно приводить к постоянной потере фокуса в проде). Как отлаживать - Проверить консоль на предупреждения про controlled/uncontrolled. - Использовать React DevTools Profiler — посмотреть, что вызывает перерисовку/ремонт компонента. - Добавить лог в жизненный цикл / хук: `useEffect(() => { console.log('mount'); return () => console.log('unmount'); })` — поймёте, ремонтится ли input. - Логировать пропы и сравнить их по ссылочной и по значению: если пропы меняются по ссылке (новая функция/объект), это причина. - Проверить `key` у элемента/родителя — генерируется ли он динамически. - Для проблем с курсором при форматировании — логировать `selectionStart/selectionEnd` до и после обновления. Практики и решения (с пояснениями) - Стабильный key - Не ставьте `key={Math.random()}` или `key={value}` (если value меняется при вводе) — это вызывает ремонт. Используйте постоянный уникальный идентификатор записи. - Меморизация компонента - Оберните компонент в `React.memo`, чтобы избежать лишних перерисовок, если пропы не поменялись по значению: ``` export default React.memo(function Text({ value, onChange }) { return onChange(e.target.value)} />
}) ``` - При необходимости используйте второй аргумент с глубоким сравнением. - Стабильные обработчики/пропы (useCallback/useMemo) - Не передавайте inline‑функции/объекты, если это вызывает лишние рендеры: мемоизируйте `onChange` в родителе через `useCallback`. ``` const onChange = useCallback(v => setState(v), [setState]) ``` - Разделение состояния (локальное + синхронизация) - Держите внутреннее локальное состояние для быстрого отклика ввода и синхронизируйте с родителем по `onBlur` или с debounce: ``` function Text({ value: outerValue, onChange }) { const [local, setLocal] = useState(outerValue) useEffect(() => setLocal(outerValue), [outerValue]) return setLocal(e.target.value)} onBlur={() => onChange(local)} />
} ``` - Это снижает зависимость ввода от часто меняемого родительского стейта и уменьшает лаг/прыжки. - Управление курсором при форматировании - Перед изменением сохраните `selectionStart/selectionEnd`, после обновления восстановите через ref. Это важно для форматирования (телефон, пробелы, разделители), чтобы не «прыгал» кареточный индекс. ``` const ref = useRef(null) const pos = ref.current?.selectionStart // обновить value if (ref.current && pos != null) ref.current.setSelectionRange(pos, pos) ``` - useRef для сохранения DOM‑рефа и ручного фокуса — применять осторожно - В редких случаях можно восстановить фокус вручную (`ref.current.focus()`), но лучше устранить причину ремонта, чем программно возвращать фокус, чтобы не сломать ожидаемое поведение для скринридеров. Доступность (A11y) - Не перемонтируйте input при переключениях — стабильный DOM помогает скринридерам. - Стабильные `id` для label и aria‑атрибутов. Не удаляйте/заменяйте input при обновлениях, иначе a11y‑контекст теряется. - При автоматических подсказках/состояниях вынесите управление фокусом явно: пользовательский ввод не должен неожиданно терять фокус из‑за обновления UI. - Если используете debounce/локальное состояние, убедитесь, что сообщения об ошибках/валидации озвучиваются корректно (`aria-live`), но не мешают фокусу. Короткие советы «что проверить первым» - Нет ли у input меняющегося `key`. - Не ремонтируется ли компонент (лог mount/unmount). - Не передаётся ли каждый рендер новая функция/объект в пропсах. - Нет ли форматирования, которое не сохраняет позицию курсора. - Не появляется ли предупреждение про controlled/uncontrolled. Если нужно — пришлите код родителя и дерево компонентов; покажу конкретно, где ремонт/прыжок фокуса.
Причины потери фокуса
- Компонент полностью размонтируется и монтируется заново (например, изменился `key`, или возвращается другая ветка JSX). При ремонте DOM‑элемент теряет фокус.
- Элемент заменяется другим элементом (изменился тип/структура дерева) — React создаёт новый input.
- Принудительная переинициализация value/значения (в том числе форматирование строки), из‑за которой курсор/выделение сбрасываются.
- Переход между контролируемым и неконтролируемым режимом (value становится `undefined`/`null` или обратно) — React даёт предупреждение и поведение ненадёжно.
- Частые переотрисовки родителя, передача новой (ненужной) ссылки пропов (функция/объект) — дочерний компонент заново рендерится/реинициализируется.
- В DEV: React.StrictMode может двойной рендер в разработке (но это не должно приводить к постоянной потере фокуса в проде).
Как отлаживать
- Проверить консоль на предупреждения про controlled/uncontrolled.
- Использовать React DevTools Profiler — посмотреть, что вызывает перерисовку/ремонт компонента.
- Добавить лог в жизненный цикл / хук: `useEffect(() => { console.log('mount'); return () => console.log('unmount'); })` — поймёте, ремонтится ли input.
- Логировать пропы и сравнить их по ссылочной и по значению: если пропы меняются по ссылке (новая функция/объект), это причина.
- Проверить `key` у элемента/родителя — генерируется ли он динамически.
- Для проблем с курсором при форматировании — логировать `selectionStart/selectionEnd` до и после обновления.
Практики и решения (с пояснениями)
- Стабильный key
- Не ставьте `key={Math.random()}` или `key={value}` (если value меняется при вводе) — это вызывает ремонт. Используйте постоянный уникальный идентификатор записи.
- Меморизация компонента
- Оберните компонент в `React.memo`, чтобы избежать лишних перерисовок, если пропы не поменялись по значению:
```
export default React.memo(function Text({ value, onChange }) {
return onChange(e.target.value)} /> })
```
- При необходимости используйте второй аргумент с глубоким сравнением.
- Стабильные обработчики/пропы (useCallback/useMemo)
- Не передавайте inline‑функции/объекты, если это вызывает лишние рендеры: мемоизируйте `onChange` в родителе через `useCallback`.
```
const onChange = useCallback(v => setState(v), [setState])
```
- Разделение состояния (локальное + синхронизация)
- Держите внутреннее локальное состояние для быстрого отклика ввода и синхронизируйте с родителем по `onBlur` или с debounce:
```
function Text({ value: outerValue, onChange }) {
const [local, setLocal] = useState(outerValue)
useEffect(() => setLocal(outerValue), [outerValue])
return setLocal(e.target.value)} onBlur={() => onChange(local)} /> }
```
- Это снижает зависимость ввода от часто меняемого родительского стейта и уменьшает лаг/прыжки.
- Управление курсором при форматировании
- Перед изменением сохраните `selectionStart/selectionEnd`, после обновления восстановите через ref. Это важно для форматирования (телефон, пробелы, разделители), чтобы не «прыгал» кареточный индекс.
```
const ref = useRef(null)
const pos = ref.current?.selectionStart
// обновить value
if (ref.current && pos != null) ref.current.setSelectionRange(pos, pos)
```
- useRef для сохранения DOM‑рефа и ручного фокуса — применять осторожно
- В редких случаях можно восстановить фокус вручную (`ref.current.focus()`), но лучше устранить причину ремонта, чем программно возвращать фокус, чтобы не сломать ожидаемое поведение для скринридеров.
Доступность (A11y)
- Не перемонтируйте input при переключениях — стабильный DOM помогает скринридерам.
- Стабильные `id` для label и aria‑атрибутов. Не удаляйте/заменяйте input при обновлениях, иначе a11y‑контекст теряется.
- При автоматических подсказках/состояниях вынесите управление фокусом явно: пользовательский ввод не должен неожиданно терять фокус из‑за обновления UI.
- Если используете debounce/локальное состояние, убедитесь, что сообщения об ошибках/валидации озвучиваются корректно (`aria-live`), но не мешают фокусу.
Короткие советы «что проверить первым»
- Нет ли у input меняющегося `key`.
- Не ремонтируется ли компонент (лог mount/unmount).
- Не передаётся ли каждый рендер новая функция/объект в пропсах.
- Нет ли форматирования, которое не сохраняет позицию курсора.
- Не появляется ли предупреждение про controlled/uncontrolled.
Если нужно — пришлите код родителя и дерево компонентов; покажу конкретно, где ремонт/прыжок фокуса.