Привет, Хабр! Меня зовут Анна, я JS-разработчик в компании SimbirSoft и занимаюсь разработкой веб-приложений на React. Эту статью я посвящаю тем, кто занимается разработкой, сталкивается с нестандартными задачами и переживает, что нашу профессию может вскоре заменить искусственный интеллект (ИИ). Я поделюсь решением задачи, связанной с динамическими размерами блока, — проблемой, с которой наверняка может столкнуться в своей работе любой frontend-разработчик.

Почему же я назвала статью именно «Долгий путь к ResizeObserver»?

Возможно, я и слышала раньше про этот API, но когда передо мной встала конкретная задача (описанная ниже), я о нем даже не вспомнила. Мне пришлось пробовать сначала одно решение, потом другое — и лишь в третью очередь я пришла к нужному инструменту. Таков мой путь — из трех шагов. Я человек, поэтому могу честно рассказать, как именно искала решения, в отличие от ИИ. Надеюсь, моя статья поможет вам быстро и эффективно справиться с похожей задачей, а заодно придаст уверенности в собственных силах. Я убеждена: ответы на вопросы находятся не только в нашей голове, но и в окружающем мире, а человек, в отличие от ИИ, способен чувствовать, искать и находить их самым неожиданным для себя образом.

Итак, приступим ?

Перед нами лейаут, который состоит из трех блоков:

Высота первого и второго блоков может меняться, пока пользователь взаимодействует со страницей, а высота третьего должна подстраиваться динамически. Если высота второго блока небольшая, тогда третий блок будет иметь фиксированную высоту (200 px). Все три блока состоят из разных частей, данные в которые поступают изолированно.

Основная задача состоит в том, чтобы отслеживать высоту первого и второго блоков и, в зависимости от их значений, задавать высоту третьему блоку. Задавать ее нужно обязательно (и здесь лучше не полагаться на вычисление CSS), поскольку в третьем блоке несколько табов с контентом разной высоты, и при переключении между ними высота не должна «прыгать». Высота третьего блока не может быть меньше 200 px.

Итак, у моей статьи две цели:

  1. Рассказать, каким способом решила эту задачу — может быть полезно тем, кто столкнется с подобной ситуацией. 

  2. Рассказать, как именно я решала эту задачу — то есть описать процесс поиска решения с помощью естественного интеллекта (части статьи, посвященные этой цели, будут выделены курсивом). 

До сих пор я решала задачи, связанные с установкой высоты элементов в зависимости от загружаемого контента, с помощью JS-функций. Поэтому первый вариант решения был таким:

// создаем функцию “получить высоту табов”
export const getHeightTabs = () => {

// с помощью нативного js получаем высоты блоков 1 и 2 (чтобы это работало, // надо задать id для этих блоков ‘block1’ и ‘block2’ соответственно)
 const heightBlock1 = document.getElementById('block1')?.clientHeight || 0;
 const heightBlock2 = document.getElementById('block2')?.clientHeight || 0;

// вычисляем высоту 3-го блока: если разница между 2-ым и 1-ым блоком 
// больше 200px, то 3-ий блок равен разнице, иначе - 200px
 const heightTabs = heightBlock2 - heightBlock1 >= 200 
                    ? heightBlock2 - heightBlock1 : 200;
 return heightTabs;
};

Вызываем эту функцию при загрузке страницы, присваиваем высоте третьего блока полученное значение.

В моей ситуации такой подход не работал с самого начала, т.к. контент первого и второго блоков загружается постепенно и зависит от множества данных. Я не придумала, как вызвать эту функцию после окончательной загрузки всех данных – это первая проблема. Вторая связана с обновлением высоты: пользователь может изменять высоту первого и второго блоков, взаимодействуя с разными частями приложения (а их 10 или 12). Поэтому вызывать функцию после обновления каждой из этих частей выглядит излишне громоздким решением. 

Эта задача находилась в бэклоге: хотя «прыгающая» высота табов раздражала и меня, и дизайнера, высокой приоритетности ей никто не присваивал — было много других проблем. Поэтому я отложила ее решение и на следующий день взялась за другую, более крупную и не менее важную задачу — изменение дизайна другой части нашего приложения (приложение большое, существует уже 7 лет, и мы его не переписываем с нуля, а поддерживаем и постепенно обновляем по частям).

Тут наступает момент моей истории, где я покажу, в чем человек превосходит ИИ, ведь я собиралась утешить тех, кто тревожится, что ИИ сможет заменить разработчиков ?

Открываю одну из папок нашего легаси и что я там вижу?

 import {useResizeDetector} from 'react-resize-detector';

Как потом выяснилось, это единственное место в нашем приложении, где использовалась эта либа. И я наткнулась на него на следующий день после того, как предприняла свою первую попытку решить проблему с высотой табов! Для меня, как для человека, это стало чудесным знаком от окружающей реальности — указанием на то, в каком направлении нужно двигаться. То есть решение «пришло» извне, а не из моих знаний. И я абсолютно уверена: человек способен находить ответы в самых неожиданных местах. А ИИ — лишь в пределах своей обучающей базы. Никто нас не заменит ?

Чудесная часть моей статьи завершена — дальше будет скучнее, но, возможно, кому-то пригодится ?

Второй способ, который я попробовала, написала кастомный хук с использованием библиотеки react-resize-detector:

// создаем хук “определитель высоты табов”
export const useTabsHeightDetect = () => {

// создаем хранилища состояний для высот 1-го и 2-го блоков
  const [hb1, setHB1] = useState(0);
  const [hb2, setHB2] = useState(0);

// создаем колбэк-функции onResize для хука useResizeDetector, где получаем
// значение высоты нужного блока и сохраняем его в хранилище
  const onResizeBlock1 = (payload) => {
const {height} = payload;
setHB1(height);
  };
  const onResizeBlock2 = (payload) => {
	const {height} = payload;
	setHB2(height);
  };

// посредством хука useResizeDetector получаем ссылку на блок,
// к которому привязано отслеживание высоты и соответствующий колбэк
  const {ref: block1Ref} = useResizeDetector({onResize: onResizeBlock1});
  const {ref: block2Ref} = useResizeDetector({onResize: onResizeBlock2});

// возвращаем обе ссылки, которые раздадим нужным блокам, и высоты блоков
// для расчета высоты третьего
  return {block1Ref , block2Ref , hb1, hb2}
}

Ссылки на первый и второй блоки раздаем в родительском компоненте, а в компоненте табов (третьем блоке) мы получаем их высоты и рассчитываем нужную.

Почему-то информация о высоте блоков не обновлялась сразу, а только при следующем рендере. Напишите в комментариях ниже почему, если знаете. 


Я попыталась разобраться, но раньше чем успела это сделать, наткнулась на замечательную статью про ResizeObserver, которая решила мою задачу так, что либа react-resize-detector мне больше не понадобилась. Внимательный читатель наверняка понял, что я снова намекаю на основную мысль своей статьи: решения приходят к человеку разными путями, и сложность этих путей, на мой взгляд, все же выше, чем у любой искусственной нейросети ?

В начале статьи авторы оптимистично дают гарантии, что после прочтения мы будем иметь четкое представление о том, как создавать отзывчивые и адаптивные веб-интерфейсы с помощью ResizeObserver API. Лично со мной так не работает, я не буду иметь четкое представление о чем-нибудь после прочтения статьи. Если я просто прочитаю статью, даже если все пойму, информация испарится из головы через 15–20 минут ? Но простим авторам такой маркетинговый ход — это их работа, зато пишут отличные статьи ?

Далее нам объясняют, что ResizeObserver — это JavaScript API, которое, как следует из названия, «наблюдает» за изменением размеров элементов веб-страницы. В статье его сравнивают с тренером, который наблюдает за командой на поле и может заметить даже усталость игрока или его возможные травмы. На мой взгляд, аналогия не совсем удачная — я бы не стала сравнивать API с тренером. Observer — это все-таки обозреватель, хотя, возможно, имелось в виду, что никто не следит за игроками так внимательно, как тренер, а наше API такое же внимательное и заботливое. Может быть, но мне кажется, что это не совсем верно и довольно абстрактное сравнение.  Тем не менее, заметив изменения элемента, ResizeObserver может вызвать колбэк, который разработчик может задать.


Далее в статье приводится сравнение ResizeObserver с медиа- и контейнер-запросами, которые разработчики обычно применяют для создания адаптивного дизайна, затем перечисляются ключевые преимущества использования ResizeObserver:

  • Мониторинг элементов: в отличие от resize events, которое отслеживает изменение размеров всего окна, ResizeObserver фокусируется на конкретных элементах страницы.

  • Улучшение производительности: позволяет избежать излишних дорогостоящих вычислений.

  • Динамическая адаптация контента: ResizeObserver — бесценный инструмент для задач, где верстка или функциональность зависят от размеров элементов, например динамические гриды, коллапсирующий сайдбар или адаптивные шрифты.

На основе приведенных примеров использования ResizeObserver я и написала хук, который отлично решил мою задачу:

// создаем хук “наблюдатель за высотой табов”, который получает ссылки
// на блоки, за которыми будет наблюдать 
export const useTabsHeightObserver = (refs) => {
// создаем хранилища для высот 1-го и 2-го блоков
  const [hb1, setHB1] = useState(0);
  const [hb2, setHB2] = useState(0);    
  // вызываем useLayoutEffect для отслеживания изменений до того,
  // как браузер перерисует экран
  useLayoutEffect(() => {
	// декомпозируем аргумент хука, получаем ссылки на блоки
        const { block1Ref, block2Ref } = refs;
	// получаем элементы из DOM за которыми наблюдаем
        const observeBlock1 = block1Ref.current;
        const observeBlock2 = block2Ref.current;
	// если элементов нет, то прекращаем наблюдение
        if (!observeBlock1 || !observeBlock2) return;
	// создаем экземпляр ResizeObserver - 
// “наблюдателя” за конкретным блоком, передаем ему колбэк для вызова
// при изменении размеров блока (сохраняем высоту блока в хранилище)
        const resizeObserverBlock1 = new ResizeObserver((entries) => {
            for (let entry of entries) {
                const { height } = entry.contentRect;
                setHB1(height);
            }
        });
        const resizeObserverBlock2 = new ResizeObserver((entries) => {
            for (let entry of entries) {
                const { height } = entry.contentRect;
                setHB2(height);
            }
        });
	// призываем наблюдателей наблюдать за конкретными элементами
        resizeObserverBlock1.observe(observeBlock1);
        resizeObserverBlock2.observe(observeBlock2);
	// увольняем наблюдателей после исчезновения элементов наблюдения
        return () => {
            resizeObserverBlock1.unobserve(observeBlock1);
            resizeObserverBlock2.unobserve(observeBlock2);
        };
    }, [refs]);
  // вычисляем и возвращаем высоту третьего блока
  const tabsHeight = hb2 - hb1;
  return tabsHeight;
};

Увидеть работу моего хука можно в песочнице.

Заключение

В заключение хочу еще раз подчеркнуть основную идею своей статьи. Конечно, мне хотелось поделиться тем, какой замечательный хук я написала, вдохновившись ResizeObserver. Но за этим стоит нечто более важное. Когда мы погружаемся в обдумывание решения той или иной задачи, то оно уже существует в окружающем нас мире — в том, что мы видим ежедневно, просто живя своей обычной жизнью. Поэтому если перед вами стоит задача, просто дайте себе время подумать и оглянитесь вокруг. Решение рядом. Мы лучше ИИ, поскольку наша база данных — это весь мир ? Так мы победим ?

Очень жду ваших комментариев. Спасибо за внимание! 

Больше авторских материалов для frontend-разработчиков от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.

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


  1. Vitaly_js
    21.07.2025 10:36

    Я бы на вашем месте пример все таки отредактировал. Вдруг кто-то подумает, что у вас что-то такое в продакшене.

    Тут два момента. Первый, когда работаешь с рефами нет никакого смысла прокидывать их в зависимости (у вас этого конечно нет, но ваше решение вдохновлено чем-то подобным). Линтеры об этом знают, поэтому "кричать" не будут. И второй, когда работаешь с хуками нельзя конфигурировать их объектными литералами. В вашем примере, каждый рендер приводит к отработке useLayoutEffect, что вредно.

    И есть еще один момент. Вы сделали вот такую штуку:

    const [hb1, setHB1] = useState(0);
    const [hb2, setHB2] = useState(0); 

    Это означает, что при каждом изменении размера у вас перерисовывается весь компонент. Это, конечно, мелочь, если размеры меняются редко, но все таки смысла для ряда случаев в этом нет никакого.

    п.с.: за оптимизм пальчик вверх я вам поставлю.


  1. adminNiochen
    21.07.2025 10:36

    Формулировка задачи (особенно картинки) выглядит, как будто её можно решить через css без обсерверов, просто надо базово знать гриды


    1. lolenko
      21.07.2025 10:36

      ага, в следующий раз автору предлагаю начать именно с этого способа) кажется на флексах минут 10 https://codesandbox.io/p/sandbox/z9lgd6