Привет, на связи снова я – React-разработчик Дмитрий. Сегодня отвлечемся от теории и разберем конкретный случай и какое решение для него использовалось.
При работе с готовыми UI-библиотеками часто возникает небольшая проблема — компонент хорош, но его поведение нельзя настроить под требования конкретного случая. Я столкнулся с этим на практике, когда в используемой версии UI Kit не было возможности кастомизировать тексты выводимых ошибок, а бизнес-требования четко задали формулировки.
В моём случае это был компонент для загрузки файлов. Он работал исправно — позволял выбрать файл, проверял его размер и формат и при ошибках показывал сообщение. Проблема была только в тексте этих сообщений. Нужно было использовать просто другую формулировку текста ошибки.
Собственно, выглядит компонент загрузки файла вот так:

Когда пользователь пытается прикрепить файл, input по умолчанию фильтрует файлы в папке и показывает только разрешённые форматы. Но всегда можно выбрать «Все типы» или перетащить файл в область загрузки. В этом случае появится ошибка о том, что файл имеет неверный формат.
И что? Патчить библиотеку? Форкать? Лезть внутрь неё? Написать новый компонент? Всё это выглядело как стрельба себе в ногу, ради одного места в проекте. Нужен был способ точечно изменить поведение компонента, не ломая его изнутри, потому что во всех других частях проекта формулировки совпадали с «хотелками».
Поиск решения. Почему именно MutationObserver?
Первым делом, конечно же, я попытался найти штатный способ изменить тексты ошибок — проверил документацию и пропсы компонента, поискал возможность переопределения сообщений. Но, к сожалению, ничего из этого в используемой версии UI Kit не было. Компонент выдавал свои тексты, и повлиять на них извне было нельзя.
Нужен был способ подслушать, когда библиотека вставляет в DOM сообщение об ошибке, и заменить текст на лету, без вторжения в её код. И здесь на помощь пришёл MutationObserver. Идея решения стала простой: подписаться на область загрузчика файлов и подменять текст ошибок сразу в момент их появления.
Коротко о MutationObserver. Что это и как работает?
MutationObserver — это встроенный в браузер API, который позволяет отслеживать изменения в DOM-дереве. С его помощью можно слушать добавление, удаление или изменение элементов, атрибутов и текста без постоянного опроса DOM через таймеры.
Принцип работы:
Создаём наблюдатель, передавая функцию обратного вызова, которая будет вызываться при изменениях.
Выбираем элемент DOM для наблюдения.
Указываем, какие типы изменений отслеживать через опции.
Основные параметры наблюдения:
childList — следить за добавлением или удалением дочерних элементов.
attributes — отслеживать изменения атрибутов элемента.
characterData — отслеживать изменения текстового содержимого.
subtree — включить отслеживание всех вложенных элементов, а не только прямых детей.
attributeFilter — массив атрибутов, за которыми нужно наблюдать.
attributeOldValue / characterDataOldValue — сохранять старое значение для сравнения.
Простой пример выглядит так:
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
console.log(mutation);
});
});
observer.observe(document.body, { childList: true, subtree: true });
Нюансы работы с MutationObserver
MutationObserver асинхронен: его callback вызывается не мгновенно, а после того как текущий стек операций DOM завершён. Это значит, что добавленные узлы или изменения атрибутов станут видны уже после завершения текущего JS-кода.
Важно учитывать этот момент, если планируется последовательная работа с динамическими элементами сразу после их появления.
Реализация: подмена текста ошибок на лету
Решение оказалось довольно простым. Так как проект на React, правильно будет разместить наблюдатель внутри useEffect, чтобы он подключался при монтировании компонента и отключался при размонтировании.
Ключевой вопрос — за какой областью DOM следить?
Изначально я выбрал document.body, чтобы гарантированно поймать появление ошибки от UI Kit в любом месте страницы. Далее решил обернуть свой компонент в div c id #uploaderContainer. Лучше наблюдать именно за ним — это снизит нагрузку и исключит лишние срабатывания. Здесь, наверное, следует уточнить, что, если будет много подобных наблюдателей или больших DOM-деревьев, может потребоваться оптимизация (например, debounce).
Принцип работы такой:
Подписываемся на изменения DOM.
Отлавливаем добавленные узлы в addedNodes
Ищем внутри них элементы с классом .m-fileUploader__errorText – это контейнер со списком ошибок, который рисует UI Kit.
Проверяем их текст и при необходимости заменяем на тот, который нужен
Собственно, код получился следующим:
useEffect(() => {
// 1. Создаём MutationObserver — объект, который следит за изменениями DOM
const obs = new MutationObserver((mutations) => {
// 2. Проходим по всем мутациям, которые зафиксировал наблюдатель
mutations.forEach((mutation) => {
// 3. Рассматриваем только добавленные элементы
mutation.addedNodes.forEach((node) => {
// 4. Нас интересуют только HTML-элементы (а не текстовые узлы)
if (node instanceof HTMLElement) {
// 5. Ищем внутри нового узла элементы с классом ошибки
node.querySelectorAll('.m-fileUploader__errorText').forEach((el) => {
// 6. Проверяем текст ошибки и подменяем на требуемый
if (el.textContent?.includes('Формат файла')) {
el.textContent = 'Ошибка. Недопустимый формат файла';
}
if (el.textContent?.includes('Размер файла')) {
el.textContent = 'Ошибка. Размер файла превышает допустимый лимит';
}
});
}
});
});
});
// 7. Определяем контейнер, за которым будем наблюдать
const container = document.getElementById('uploaderContainer');
if (container) {
obs.observe(container, {
childList: true, // следим за добавлением/удалением дочерних элементов
subtree: true // следим и за всеми вложенными элементами
});
}
// 8. При размонтировании компонента отключаем наблюдатель,
// чтобы не было утечек памяти
return () => obs.disconnect();
}, []);
Подведем промежуточные итоги:
MutationObserver отлично подходит для случаев, когда нужно подслушивать изменения DOM и что-то поменять, но при этом стандартные события или перерендеринг React не подходят.
А также MO помогает наблюдать лучше за узкой областью, а не за всем документом — это повышает производительность.
Проверяем addedNodes, потому что UI Kit добавляет новые элементы с ошибками динамически.
И не забываем про disconnect() в React, чтобы при размонтировании компонента не оставалось активных наблюдателей и не происходило утечек памяти.
Почему это лучше, чем «ломать» UI Kit
Когда сталкиваешься с ограничениями библиотек, первое, что приходит в голову — форкнуть компонент или лезть внутрь исходников. Такой подход работает, но создаёт множество проблем, а решение с MutationObserver:
- Позволяет модифицировать поведение компонента без создания отдельной версии библиотеки. Сама библиотека остаётся девственной, а код проекта — проще и легче поддерживаемым.
- Оставляет разработчику возможность обновлять библиотеку. Если бы мы внесли правки напрямую в исходники, любая новая версия UI Kit могла бы сломать наш патч. С MutationObserver компонент обновляется автоматически, а текст ошибок по-прежнему подменяется на нужный. Да, есть небольшие нюансы с классом элемента, в который обернуты ошибки, но тут ничего не поделать: id там нет. Но если бы был – привязка к нему была бы более стабильным решением.
- Всё происходит на лету в конкретном месте — мы наблюдаем за нужным контейнером и подменяем только требуемые тексты. Остальная часть библиотеки остаётся нетронутой, и риск сломать что-то в другом месте отсутствуют.
Такой подход позволяет решать реальные задачи кастомизации без больших затрат времени и ресурсов, сохраняя стабильность и обновляемость проекта в местах, где это необходимо не системно, а точечно.
Возможные улучшения
Решение с MutationObserver работает, но можно набросать несколько способов улучшить его при необходимости. Тут уже дело каждого разработчика в отдельности или каких-то корпоративных стандартов, но общие моменты можно выделить следующие:
Вынести тексты ошибок в конфиг или объект локализации
Вместо жёстко прописанных строк в коде использовать объект с ключами и значениями ошибок. Это упрощает поддержку и добавляет возможность изменения формулировок без правки кода.
Добавить проверку, чтобы подмена срабатывала один раз
Если компонент UI Kit перерисовывает элементы с ошибками несколько раз, текст может перезаписываться лишний раз. Можно проверять текущий текст или использовать флаг, чтобы менять его только один раз.
Мемоизация или оборачивание MutationObserver в useCallback
В React можно обернуть создание наблюдателя в useCallback или мемоизировать его, если планируется повторное использование. Это уменьшает шанс лишних подписок и утечек памяти, особенно при динамическом монтировании и размонтировании компонентов.
Следить за конкретной областью DOM
Всегда лучше подключать наблюдатель к конкретному контейнеру, а не к всему document.body. Это снижает нагрузку на браузер и уменьшает количество ненужных срабатываний.
Подведем итоги
Проблема решена, ни одна библиотека не пострадала, текст ошибок подменяется точечно, а заказчик доволен! Всё получилось просто, безопасно и, как говорится, без лишних танцев с бубном. Цель достигнута!