Перейти к содержимому

react

5 posts with the tag “react”

Когда ре-рендер нужен, но никак не случается

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

Итак, что было? В нашем App.tsx:

<Router history={history}>
<UuiContext.Provider value={services}>
<Header />
<Routes />
<Footer />
</UuiContext.Provider>
</Router>

В Header.tsx — навигация:

// компонент из библиотеки, у него есть подсветка активного пункта меню.
// заглядывает в uuiContext от UuiContext.Provider и проверят, активная ли линка через history
<MainMenu>
<MainMenuButton caption='Home' link={{pathname: '/' } />
<MainMenuButton caption='Dashboard' link={{pathname: '/dashboard' } />
</MainMenu>

И вроде всё (ну, почти всё) работает: по кликам в MainMenu урл обновляется, контент перерисовывается. Вот только нет выделения активного элемента в MainMenu.

Начинаю разбираться, почему — ставлю точку остановы в фукнцию в MainMenu.tsx, где определяется, активная ли линка. И тут начинается интересное: при переходе по кнопкам и обновлении урла функция не вызывается.

Добавляю console.log(‘render’) в Header.tsx. Что ж, он не пере-рендеривается, так что логично, что ничего не происходит. Но ведь мы же вроде обновляем историю, а в uuiContext.uuiRouter.history — как раз всё обновляется… Почему же дочерние элементы UuiContext.Provider не обновляются?

Тут я пошла неверным путём: попыталась отладиться в контекстах, потому что заподозрила, что у меня где-то создается несколько инстансов и происходит путаница. Ничего не нашла, естественно.

После блуждания по контекстам, в мою голову пришла мысль: содержимое страницы обновляется за счет изменения актуального Route — поэтому часть в Routes перерендеривается нормально. Что, если Header положить внутрь каждой страницы, соответствующей роуту? Это помогло, однако в таком случае Header каждый раз при изменении роута unmount и mount. Выглядит некрасиво и излишне.

И вот тут наконец хороший вопрос пришел в голову: как React Router определяет, что надо что-то пере-рендерить? И вот что я выяснила:

  • мы обновляем history
  • history object имеет постоянную ссылку
  • useHistory поэтому все время будет возвращать один и тот же объект, поэтому useEffect c history.location не отработает при изменении location
  • зато вот useLocation и useParams следят за изменениями локации, поэтому как только мы их используем — наш компонент начинает пере-рендериваться при изменениях

Самое противное, что у меня уже была подобная проблема, но вспомнила о ней и об этом моменте я только тогда, когда “заново” нашла решение. Записываю сюда, чтобы больше не забывать 🤪

Миф про ре-рендеры

Вам когда-нибудь говорили, что вот так передавать массив — плохая практика?

<Component levels={[1, 2, 3]} />

Ведь массив будет пересоздаваться в JSX при каждом ре-рендере, то есть пропсы будут меняться и запускать ненужные ре-рендеры Component?

И вы наверняка слышали, что нужно использовать useMemo (const levels = useMemo(() => [1, 2, 3], [])) или объявить массив вне компонента?

Я слышала это много раз. И следовала этому принципу.

Даже когда я столкнулась с кейсами, что при передаче в пропсы ref объекта и обновлении этого объекта потом — компонент не перерисовывается, я всё равно продолжала избегать передачи вот таких массивов и объектов как пропсов. Я просто не соединила в своей голове эти две простые вещи: если обновление пропсов должно приводить к ре-рендеру, тогда это должно работать и с ref, не так ли?

Сейчас я читалю Advanced React от Nadia Makarevich, и автор пишет, что одно из главных заблуждений:

Миф про ре-рендеры: Компонент перерисовывается, когда изменились пропсы.

React обновляется, когда происходит обновление стейта. И в этом случае, он перерендерит все вложенные компоненты, изменились их пропсы или нет.

В контексте ре-рендеров, изменились пропсы компонента или нет важно только в одном случае: если этот компонент обернут в HOC React.memo.

Посмотрите на примере с кодом: https://codesandbox.io/p/sandbox/props-rerender-dcq97c.

И всё! Нужно поменять способ мышления — перенести фокус с изменения пропсов на анализ изменений стейта.

P.S. Сама я для константных объектов / массивов предпочитаю создавать constants.ts и объявлять их там, но это дело личного вкуса и никак не связано с ре-рендерами.

HOC или не HOC?

В наборе кат для изучения паттернов попалась одна про Higher-Order Component(HOC) в React — truncate paragraph with HOC in React. Плюс недавно разбиралась с паттерном Декоратор, и примером применения для React как раз считается HOC.

И вот я задумалась: а действительно ли хоки актуальны?

Чаще всего я вижу хоки, когда работаю с кодом, написанным 3-4 года назад, но в новом коде я практически их не вижу. Опять же, в старой документации React есть целая страница, посвященная HOC и примерам использования, а вот в новой классной документации ничего нет.

Кажется, что кастомные хуки вытеснили хоки. Что такого особенного ты можешь сделать с HOC, что не можешь, просто написав кастомный хук?

Лично я предпочитаю кастомные хуки или даже классы с вынесенной в них бизнес-логикой. Для меня это более очевидный и поддерживаемый подход.

Сегодня узнала, что можно в ref передавать callback

От моего внимания как-то ускользнуло, что в React можно передавать функцию-callback как ref для элемента, а не только объект, созданный с помощью useRef.

const scroller = (node: HTMLDivElement) => {
if (!node) return;
node.scrollIntoView({ behavior: "smooth" });
};
// somewhere in component
<div ref={scroller} />

Как это работает:

  • Когда элемент добавляется в DOM, React вызывает функцию-callback и передает в неё DOM-узел как аргумент.
  • Когда элемент удаляется из DOM, React вызывает эту функцию с аргументом null.
  • Callback также вызывается каждый раз, когда передается новая функция (например, при каждом рендере, если она определена как обычная функция).

Следите за дивами

Немного о задаче, которую сегодня решила на работе.

У нас есть таблица с фильтрами, которая выводит список визитов в офисы. Некоторые визиты требуют дополнительного внимания менеджеров, например: выдан пропуск посетителю, визит закончился, а пропуск не вернули.

Задача была подсветить такие визиты в таблице и дать возможность менеджерам легко отфильтровать их. Я добавила подсветку для строк, а над таблицей - баннер с кнопкой “Показать проблемые” (условно). Я также добавила функциональность, чтобы фильтр по проблемным визитам не сохранялся (мы запоминаем выбранные фильтры пользователя). Всё это работало - пока не появились проблемы с фильтрами.

Большая часть фильтров у нас - это дропдауны со списком опций. На это ветке эти дропдауны стали медленно работать, и после 3-4 быстрых кликов страница ломалась. В консоли появлялись ошибки о “Maximum update depth exceeded” и рекомендации проверить useEffects и их зависимости. Стек вызовов указывал на компонент дропдауна из библиотеки компонентов нашей компании.

Что я стала делать

Я начала с попытки понять, что происходит и где может быть проблема. Посмотрела изменения в коде. Сложности добавляло то, что в этой же ветке я стала использовать код из другого пакета в нашей монорепе (я использую npm workspaces), и изменений было много.

Естественно, я попыталась отлаживаться. Однако было слишком много вызовов, компонент дропдауна выглядел нормально и не было в нем useEffects.

Я погуглила и спросила Claude. Время шло. Я решила перейти к одному из моих любимых методов отладки - удалению частей кода.

Для начала я хотела понять, не из-за ли нового пакета возникла проблема. Создала новую ветку от develop и перенесла минимальные изменения туда. Фильтры всё еще работали отвратительно и ломались.

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

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

Наконец, я нашла проблему: div, который оборачивал таблицу и баннер над ней. Когда я заменила его на фрагмент, проблема исчезла.