Привет, на связи снова я – React-разработчик Дмитрий. Сегодня отвлечемся от теории и разберем конкретный случай и какое решение для него использовалось.

При работе с готовыми UI-библиотеками часто возникает небольшая проблема —  компонент хорош, но его поведение нельзя настроить под требования конкретного случая. Я столкнулся с этим на практике, когда в используемой версии UI Kit не было возможности кастомизировать тексты выводимых ошибок, а бизнес-требования четко задали формулировки.

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

Собственно, выглядит компонент загрузки файла вот так:

Когда пользователь пытается прикрепить файл, input по умолчанию фильтрует файлы в папке и показывает только разрешённые форматы. Но всегда можно выбрать «Все типы» или перетащить файл в область загрузки. В этом случае появится ошибка о том, что файл имеет неверный формат.

И что? Патчить библиотеку? Форкать? Лезть внутрь неё? Написать новый компонент? Всё это выглядело как стрельба себе в ногу, ради одного места в проекте. Нужен был способ точечно изменить поведение компонента, не ломая его изнутри, потому что во всех других частях проекта формулировки совпадали с «хотелками».

Поиск решения. Почему именно MutationObserver?

Первым делом, конечно же, я попытался найти штатный способ изменить тексты ошибок — проверил документацию и пропсы компонента, поискал возможность переопределения сообщений. Но, к сожалению, ничего из этого в используемой версии UI Kit не было. Компонент выдавал свои тексты, и повлиять на них извне было нельзя.

Нужен был способ подслушать, когда библиотека вставляет в DOM сообщение об ошибке, и заменить текст на лету, без вторжения в её код. И здесь на помощь пришёл MutationObserver. Идея решения стала простой: подписаться на область загрузчика файлов и подменять текст ошибок сразу в момент их появления.

Коротко о MutationObserver. Что это и как работает?

MutationObserver — это встроенный в браузер API, который позволяет отслеживать изменения в DOM-дереве. С его помощью можно слушать добавление, удаление или изменение элементов, атрибутов и текста без постоянного опроса DOM через таймеры.

Принцип работы:

  1. Создаём наблюдатель, передавая функцию обратного вызова, которая будет вызываться при изменениях.

  2. Выбираем элемент DOM для наблюдения.

  3. Указываем, какие типы изменений отслеживать через опции.

Основные параметры наблюдения:

  • 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).

Принцип работы такой:

  1. Подписываемся на изменения DOM.

  2. Отлавливаем добавленные узлы в addedNodes

  3. Ищем внутри них элементы с классом .m-fileUploader__errorText – это контейнер со списком ошибок, который рисует UI Kit.

  4. Проверяем их текст и при необходимости заменяем на тот, который нужен

Собственно, код получился следующим:

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. Это снижает нагрузку на браузер и уменьшает количество ненужных срабатываний.

Подведем итоги

Проблема решена, ни одна библиотека не пострадала, текст ошибок подменяется точечно, а заказчик доволен! Всё получилось просто, безопасно и, как говорится, без лишних танцев с бубном. Цель достигнута!

Комментарии (0)