Привет, друзья! В этой статье я хочу поговорить с вами об управлении состоянием и повторном рендеринге в React.js. Что такое состояние и зачем им управлять? Состояние/state можно определить как любые данные, которые влияют на рендеринг/rendering компонентов. Состояние хранится в реактивных переменных/reactive variables ](в терминологии RxJS). Управление состоянием/state management — это механизм, позволяющий создавать реактивные переменные, фиксировать изменения их значений и уведомлять об этом "заинтересованные" компоненты. Как правило, такой механизм реализуется с помощью паттерна Издатель-Подписчик/Publisher-Subscriber/Pub-Sub. Реакция компонентов на изменение состояния часто, но далеко не всегда завершается добавлением/модификацией/удалением HTML-элементов в/из DOM. Как известно, операции, связанные с манипулированием DOM, являются очень медленными. Это обуславливает необходимость грамотного управления состоянием приложений. Состояние бывает 2 видов: В свою очередь, распределенное состояние условно также можно разделить на 2 вида: Отличная иллюстрация того, чем следует руководствоваться при определении вида состояния: 
 Существует еще один подход к управлению состоянием, предназначенный для приложений, где большая часть состояния хранится на сервере. Такой подход предлагает, в частности, библиотека React Query. Управление распределенным состоянием клиента в этом случае, как правило, реализуется за счет кеширования. Условно данный подход можно назвать смешанным/mixed. Чуть позже мы рассмотрим паттерн, позволяющий управлять состоянием приложения наиболее простым и эффективным способом. Что такое рендеринг и почему он происходит? Рендеринг — это вычисление компонентов (их структуры, дочерних компонентов, свойств и других данных), часто завершающаяся добавлением/модификацией/удалением HTML-элементов в/из DOM. Выполнение кода функции (компонента) не всегда завершается отрисовкой/перерисовкой компонента в DOM. Для определения необходимости в манипулировании DOM React использует множество эвристических техник: объект виртуальный DOM/virtual DOM или, если быть точнее, волокно/fiber, алгоритм согласование DOM/DOM diffing (опять же, в настоящее время речь идет о согласовании волокон), атрибут ключ/key и т.д. Рендеринг бывает 2 видов: Для управления первоначальным рендерингом предназначены такие вещи как утилита lazy и компонент верхнего уровня Suspense, позволяющие выполнять разделение кода/code splitting, т.е. загружать (в этом React помогает Webpack) и выполнять только тот JS-код, который используется приложением в данный момент, а также условный рендеринг/conditional rendering компонентов, когда рендерятся только те компоненты, которые "соответствуют" текущему состоянию приложения. Повторный рендеринг происходит в следующих случаях: Отличное визуальное руководство по повторному рендерингу в React. Потребление контекста/context consuming также приводит к повторному рендерингу компонента. Это происходит при изменении передаваемых через контекст значений, которые используются (потребляются) компонентом. Поскольку чаще всего через контекст передается объект, это происходит почти всегда. Для управления повторным рендерингом предназначены такие вещи как: Для мониторинга повторного рендеринга предназначены такие вещи как: Чуть позже мы рассмотрим примеры использования этих инструментов. Как сделать хорошо? Разработаем простое React/TypeScript-приложение и научимся управлять его состоянием, а также контролировать рендеринг его компонентов. Создаем шаблон приложения с помощью create-react-app: Наше приложение будет состоять из 2 компонентов: RandomString и RandomHexColor. Каждый из этих компонентов будет состоять из 3 компонентов: Container, Button и компонент, объединяющий их в единое целое. Нам потребуется утилита для создания хранилища состояния (createStore), а также хук для вывода в консоль сообщений о рендеринге компонентов, начиная со второго рендеринга (useLogAfterFirstRender). Структура директории src будет следующей: Состояние будет общим (распределенным) для компонентов RandomString и RandomHexColor. Имеет смысл начать с разработки утилиты для создания хранилища. Вот чем я руководствовался при реализации данной утилиты: Начнем с определения типов для утилиты (types/index.d.ts) и дочерних компонентов: Сигнатура объекта хранилища, передаваемого утилите: Приступаем к реализации утилиты. Импортируем хуки и типы: Определяем утилиту для проксирования сеттеров: Данная утилита принимает сеттеры и декорирует их таким образом, что они, во-первых, получают состояние в качестве первого аргумента, во-вторых, обновляют состояние, возвращая объект, ключи которого должны совпадать с ключами модифицируемых частей состояния. Сеттер также может вернуть модифицированное состояние в целом. Определяем утилиту для создания хранилища: Используем данную утилиту для создания хранилища в App.tsx. Импортируем стили, тип, утилиту и компоненты: Создаем хранилище: Наше хранилище содержит 2 реактивные переменные randomString и randomHexColor и 2 метода для изменения значений этих переменных: setRandomString и setRandomHexColor, соответственно. Создаем хранилище, экспортируем хуки и оборачиваем компоненты в провайдер: Не забываем установить sass: yarn add -D sass или npm i -D sass. Хук для вывода в консоль сообщений о рендеринге (hooks/useLogAfterFirstRender.ts): Теперь займемся компонентами. Контейнер для случайной строки (RandomString/Container.tsx): Кнопка для установки значения строки (RandomString/Button.tsx): Компонент RandomString (RandomString/index.tsx): Компонент RandomHexColor: Выполняем повторный экспорт компонентов (components/index.ts): Посмотрим, как работает наше приложение. Выполняем команду yarn start или npm start: 
 Нажимаем на кнопку Set random string: 
 Ожидаемо меняется значение строки. Но что это? В консоли мы видим сообщения о повторном рендеринге как компонента RandomString, так и компонента RandomHexColor. Но мы же ничего не делали с RandomHexColor. Почему же он подвергся повторному рендерингу? Обратите внимание: Обратимся за помощью к профилировщику для того, чтобы понять, почему происходит повторный рендеринг, казалось бы, незаинтересованного компонента. Определим отображаемые названия компонентов: Переходим во вкладку Profiler инструментов разработчика в браузере. Настраиваем его: нажимаем на иконку шестеренки View settings и выбираем Highlight updates when components render. на вкладке General и Record why each component rendered while profiling. на вкладке Profiler: 
 
 Закрываем панель настроек профилировщика и нажимаем на стрелку Reload and start profiling в левом верхнем углу рядом с иконкой записи: 
 Нажимаем на кнопку Set random string, видим подсветку повторного рендеринга. Нажимаем на иконку записи для ее остановки и переключаем представление на Ranked chart: 
 В правом верхнем углу мы видим текущий рендеринг и общее количество рендерингов (1 / 2). Сейчас мы наблюдаем результаты первоначального рендеринга, о чем говорит сообщение при наведении курсора на любой компонент: 
 Переключаемся на результаты второго рендеринга и наводим курсор на компонент Random HEX color container: 
 Сообщение говорит нам о том, что повторный рендеринг данного компонента произошел по причине повторного рендеринга его родительского компонента, т.е. провайдера контекста (Context.Provider) (его мы видим ниже). Это верно, но лишь отчасти. Устанавливаем пакет why-did-you-render: Подключаем его в src/index.tsx для режима разработки: Обратите внимание: после перехода CRA на react-scripts v4 (где используется новый способ трансформации JSX в JS) данная утилита и ряд других какое-то время не работали. Если у вас возникнут проблемы, связанные с тем, что wdyr не выводит сообщения в консоль, поищите ответ в этом обсуждении. Регистрируем компонент RandomHexColor/Container: На всякий случай перезапускаем сервер для разработки. Снова нажимаем на кнопку Set random string и получаем гораздо более информативное сообщение: 
 Сначала мы видим, что значение переменной randomString изменилось с "" на "wpkhijfo7xi", затем, что значение, передаваемое через контекст, изменилось с { randomString: '', randomHexColor: '' } на { randomString: 'wpkhijfo7xi', randomHexColor: '' }. Так вот в чем дело! При изменении значения любой реактивной переменной генерируется новый объект. Внимание: в подавляющем большинстве случаев, при условии правильного размещения распределенных состояний, это не будет проблемой для производительности приложения. Однако предположим, что повторное вычисление компонента Random HEX color container крайне нежелательно. Допустим, мы хотим, чтобы этот компонент подвергался повторному рендерингу только при изменении значения переменной randomHexColor. Как нам этого добиться? Существует один способ, но он имеет некоторые побочные эффекты. Данный способ заключается в мемоизации компонента с помощью memo. Но для того, чтобы у нас появились пропы для сравнения, состояние необходимо поднять/lift до ближайшего родительского компонента. В нашем случае таким компонентом является RandomHexColor/index.tsx. Перепишем его следующим образом: Отрефакторим компонент RandomHexColor/Container.tsx: Возвращаемся в браузер и нажимаем Set random string: 
 Компонент Random HEX color container больше не рендерится при изменении значения переменной randomString, но за все приходится платить: теперь повторно рендерится компонент Random HEX color button, поскольку повторному рендерингу подвергается его предок (RandomHexColor). Другими словами, наша оптимизация нивелировала выгоду от предварительной мемоизации сеттеров. Мемоизируем компонент Random HEX color button с помощью memo: Снова нажимаем Set random string: 
 Теперь при изменении randomString повторно рендерится только Random string container. Мы добились, чего хотели, но еще раз повторю: наши оптимизации являются преждевременными, в них не было совершенно никакой необходимости. Последний вопрос: что если нам потребуется выполнить принудительный повторный рендеринг компонента, например, Random HEX color button. Эта задача решается за счет обновления локального состояния. Перепишем данный компонент следующим образом: Нажимаем Force update: 
 Получаем сообщение о рендеринге Random HEX color button. Пожалуй, это все, чем я хотел поделиться с вами в этой статье. Надеюсь, вы узнали что-то новое и не зря потратили время. Благодарю за внимание и happy coding!  JavaScript Developer