[Перевод] Введение в React, которого нам не хватало
React — это самая популярная в мире JavaScript-библиотека. Но эта библиотека не потому хороша, что популярна, а потому популярна, что хороша. Большинство существующих вводных руководств по React начинается с примеров того, как пользоваться этой библиотекой. Но эти руководства ничего не говорят о том, почему стоит выбрать именно React.
У такого подхода есть свои сильные стороны. Если кто-то стремится к тому, чтобы, осваивая React, тут же приступить к практике, ему достаточно заглянуть в официальную документацию и взяться за дело.
Этот материал (вот, если интересно, его видеоверсия) написан для тех, кто хочет найти ответ на следующие вопросы: «Почему React? Почему React работает именно так? С какой целью API React устроены так, как устроены?».
Почему React?
Жизнь становится проще в том случае, если компоненты не знают об обмене данными по сети, о бизнес-логике приложения или о его состоянии. Такие компоненты, получая одни и те же входные параметры, всегда формируют одни и те же визуальные элементы.
Когда появилась библиотека React — это на фундаментальном уровне изменило то, как работают JavaScript-фреймворки и библиотеки. В то время как другие подобные проекты продвигали идеи MVC, MVVM и прочие подобные, в React был выбран другой подход. А именно, тут рендеринг визуальной составляющей приложения был изолирован от представления модели. Благодаря React во фронтенд-экосистеме JavaScript появилась совершенно новая архитектура — Flux.
Почему команда разработчиков React поступила именно так? Почему такой подход лучше тех, что появились раньше него, вроде архитектуры MVC и спагетти-кода, который пишут на jQuery? Если вы из тех, кого интересуют эти вопросы, можете посмотреть это выступление 2013 года, посвящённое разработке JavaScript-приложений в Facebook.
В 2013 году компания Facebook только что завершила серьёзную работу по интеграции в свою платформу чата. Эта новая возможность была встроена практически в каждую страницу проекта, чат влиял на обычные сценарии работы с платформой. Это было сложное приложение, встроенное в другое приложение, которое и до этого нельзя было назвать простым. Команде Facebook пришлось столкнуться с решением нетривиальных задач, справляясь с неконтролируемой мутацией DOM и с необходимостью обеспечить параллельную асинхронную работу пользователей в новой среде.
Например, как заранее узнать о том, что будет выведено на экране в ситуации, когда что угодно, в любое время и по любой причине, может обратиться к DOM и внести туда изменения? Как обеспечить правильность построения того, что увидит пользователь?
Используя популярные фронтенд-инструменты, существовавшие до React, ничего такого гарантированно обеспечить было нельзя. В ранних веб-приложениях «состояние гонок» в DOM было одной из самых распространённых проблем.
Отсутствие детерминизма = параллельные вычисления + мутабельное состояние.
Мартин Одерски
Главной задачей команды разработки React было решение этой проблемы. Они с ней справились, применив два основных инновационных подхода:
- Однонаправленная привязка данных с использованием архитектуры Flux.
- Иммутабельность состояния компонента. После того, как состояние компонента установлено, оно уже не может быть изменено. Изменения состояния не затрагивают визуализированные компоненты. Вместо этого подобные изменения приводят к выводу нового представления, обладающего новым состоянием.
Самый простой обнаруженный нами способ структурирования и рендеринга компонентов, с концептуальной точки зрения, заключался в том, чтобы просто стремиться к полному отсутствию мутаций.
Том Оччино, JSConfUS 2013
Библиотека React смогла серьёзно снизить остроту проблемы неконтролируемых мутаций благодаря использованию архитектуры Flux. Вместо того чтобы присоединять к произвольному количеству произвольных объектов (моделей) обработчики событий, вызывающие обновления DOM, библиотека React дала разработчикам единственный способ управления состоянием компонента. Это — диспетчеризация действий, влияющих на хранилище данных. Когда меняется состояние хранилища, система предлагает компоненту перерендериться.
Архитектура Flux
Когда мне задают вопрос о том, почему стоит обратить внимание на React, я даю простой ответ: «Дело в том, что нам нужен детерминированный рендеринг представлений, а React значительно упрощает решение этой задачи».
Обратите внимание на то, что чтение данных из DOM ради реализации некоей логики — это анти-паттерн. Тот, кто так поступает, идёт вразрез с целью использования React. Вместо этого данные нужно читать из хранилища, а решения, основанные на этих данных, нужно принимать до того, как будут отрендерены соответствующие компоненты.
Если бы детерминированный рендеринг компонентов был бы единственной фишкой React, то одно это уже было бы замечательной инновацией. Но команда разработчиков React на этом не остановилась. Эта команда представила миру библиотеку, обладающую и другими интереснейшими, уникальными возможностями. А по мере развития проекта в React появилось ещё больше всего полезного.
JSX
JSX — это расширение JavaScript, позволяющее декларативно создавать компоненты пользовательского интерфейса. JSX обладает следующими заметными возможностями:
- Применение простой декларативной разметки.
- Код разметки расположен там же, где и код компонента.
- Реализация принципа разделения ответственностей (например — отделение описания интерфейса от логики состояния и от побочных эффектов). Причём, реализация, основанная не на использовании различных технологий (например — HTML, CSS, JavaScript).
- Абстрагирование управления изменениями DOM.
- Абстрагирование от особенностей различных платформ, для которых создают React-приложения. Дело в том, что благодаря использованию React можно создавать приложения, предназначенные для множества платформ (речь идёт, например, о разработке для мобильных устройств с использованием React Native, о приложениях для систем виртуальной реальности, о разработке для Netflix Gibbon, о создании Canvas/WebGL-интерфейсов, о проекте react-html-email).
Если, до появления JSX, нужно было декларативно описывать интерфейсы, то нельзя было обойтись без использования HTML-шаблонов. В те времена не было общепризнанного стандарта по созданию таких шаблонов. Каждый фреймворк использовал собственный синтаксис. Этот синтаксис приходилось изучать тому, кому, например, нужно было пройтись в цикле по неким данным, встроить в текстовый шаблон значения из переменных или принять решение о том, какой компонент интерфейса выводить, а какой — нет.
В наши дни, если взглянуть на разные фронтенд-инструменты, окажется, что без специального синтаксиса, вроде директивы *ngFor
из Angular, тоже не обойтись. Но, так как JSX можно назвать надмножеством JavaScript, создавая JSX-разметку можно пользоваться существующими возможностями JS.
Например, перебрать некий набор элементов можно, воспользовавшись методом Array.prototype.map
. Можно использовать логические операторы, организовывать условный рендеринг с помощью тернарного оператора. Можно пользоваться чистыми функциями, можно конструировать строки с использованием шаблонных литералов. В общем-то, тому, кто описывает интерфейсы средствами JSX, доступны все возможности JavaScript. Полагаю, что в этом заключается огромное преимущество React перед другими фреймворками и библиотеками.
Вот пример JSX-кода:
const ItemList = ({ items }) => (
{items.map((item) => (
-
{item.name}
))}
);
Правда, при работе с JSX нужно учитывать некоторые особенности, которые, поначалу, могут показаться непривычными.
- Тут используется подход к именованию атрибутов элементов, отличающийся от того, который принят в HTML. Например,
class
превращается вclassName
. Речь идёт о применении стиля именования camelCase. - У каждого элемента списка, который нужно вывести, должен быть постоянный уникальный идентификатор, предназначенный для использования в JSX-атрибуте
key
. Значение идентификатора должно оставаться неизменным в ходе различных манипуляций с элементами списка. На практике большинство элементов списков в моделях данных имеют уникальныеid
, эти идентификаторы обычно отлично показывают себя в роли значений дляkey
.
React не навязывает разработчику единственно правильный способ работы с CSS. Например, компоненту можно передать JavaScript-объект со стилями, записав его в свойство style
. При таком подходе большинство привычных имён стилей будет заменено на их эквиваленты, записанные по правилам camelCase. Но этим возможности по работе со стилями не ограничиваются. На практике я одновременно пользуюсь разными подходами к стилизации React-приложений. Выбор конкретного подхода зависит от того, что именно нужно стилизовать. Например, глобальные стили я применяю для оформления тем приложений и макетов страниц, а локальные стили — для настройки внешнего вида конкретного компонента.
Вот мои любимые возможности React, касающиеся работы со стилями:
- CSS-файлы, которые можно загружать в заголовочной части страницы. Они могут использоваться для настройки макетов страниц, шрифтов и прочих подобных элементов. Это — надёжный, работоспособный механизм стилизации.
- CSS-модули — это CSS-файлы область применения которых ограничена локальной областью видимости. Их можно импортировать непосредственно в JavaScript-файлы. Для того чтобы применять CSS-модули, нужно воспользоваться правильно настроенным загрузчиком модулей. В Next.js, например, этот механизм активирован по умолчанию.
- Пакет styled-jsx, который позволяет объявлять стили прямо в коде React-компонентов. Это напоминает использование тега
в HTML. Область видимости таких стилей можно назвать «гиперлокальной». Речь идёт о том, что стили воздействуют только на элементы, к которым они применяются, и на их дочерние элементы. При применении Next.js пакетом styled-jsx можно пользоваться без необходимости самостоятельно что-то подключать и настраивать.
Синтетические события
React даёт в наше распоряжение кроссбраузерную обёртку SyntheticEvents
, представляющую синтетические события и предназначенную для унификации работы с событиями DOM. Синтетические события весьма полезны по нескольким причинам:
- Они позволяет унифицировать особенности различных платформ, связанные с обработкой событий. Это упрощает разработку кроссбраузерных приложений.
- Они автоматически решают задачи по управлению памятью. Если вы, например, собираетесь создать некий список с бесконечной прокруткой, пользуясь лишь чистыми JavaScript и HTML, то вам придётся делегировать события или подключать и отключать обработчики событий по мере появления и скрытия элементов списка. Всё это нужно будет делать для того чтобы избежать утечек памяти. Синтетические события автоматически делегируются корневому узлу, что приводит к тому, что React-разработчикам не приходится решать задачи по управлению памятью.
- В их работе используются пулы объектов. Механизмы поддержки синтетических событий способны генерировать тысячи объектов в секунду и организовывать высокопроизводительную работу с такими объектами. Если решать подобные задачи, каждый раз создавая новые объекты, это приведёт к частой потребности в вызове сборщика мусора. А это, в свою очередь, может привести к замедлению программы, к видимым задержкам в работе пользовательского интерфейса и анимаций. Объекты синтетических событий создаются заранее и помещаются в пул объектов. Когда надобности в событии нет, оно возвращается обратно в пул. В результате разработчик может не беспокоиться о том, что сборщик мусора заблокирует главный поток JavaScript, очищая память от ставших ненужными объектов.
Обратите внимание на то, что из-за использования пула событий к свойствам синтетического события нельзя обратиться из асинхронной функции. Для реализации такой схемы работы нужно взять данные из объекта события и записать их в переменную, доступную асинхронной функции.
Жизненный цикл компонента
Концепция жизненного цикла React-компонентов ориентирована на защиту состояния компонента. Состояние компонента не должно меняться в процессе его вывода на экран. Это достигается благодаря следующей схеме работы: компонент оказывается в некоем состоянии и рендерится. Затем, благодаря событиям жизненного цикла, оказывается возможным применение к нему эффектов, можно воздействовать на его состояние, работать с событиями.
Понимание особенностей жизненного цикла компонентов React крайне важно для того чтобы разрабатывать интерфейсы и при этом не сражаться с React, а пользоваться этой библиотекой так, как задумано её разработчиками. «Сражение» с React, вроде неправильного изменения состояния компонентов или чтения данных из DOM, сводит на нет сильные стороны этой библиотеки.
В React, начиная с версии 0.14, появился синтаксис описаний компонентов, основанных на классах, позволяющий обрабатывать события жизненного цикла компонентов. В жизненном цикле React-компонентов можно выделить три важнейших этапа: Mount (монтирование), Update (обновление) и Unmount (размонтирование).
Жизненный цикл компонента
Этап Update можно разделить на три части: Render (рендеринг), Precommit (подготовка к внесению изменений в дерево DOM), Commit (внесение изменений в дерево DOM).
Структура этапа Update
Остановимся на этих этапах жизненного цикла компонента подробнее:
- Render — на этом этапе жизненного цикла компонента производится его рендеринг. Метод компонента
render()
должен представлять собой детерминированную функцию, не имеющую побочных эффектов. Эту функцию стоит рассматривать как чистую функцию, получающую данные из входных параметров компонента и возвращающую JSX. - Precommit — на этом этапе можно прочитать данные из DOM, пользуясь методом жизненного цикла компонента
getSnapShotBeforeUpdate
. Это может оказаться очень кстати, например, если перед повторным рендерингом компонента нужно узнать нечто вроде позиции скроллинга или размеров визуализированного элемента. - Commit — на этой фазе жизненного цикла компонента React обновляет DOM и рефы. Здесь можно воспользоваться методом
componentDidUpdate
или хукомuseEffect
. Именно здесь можно выполнять эффекты, планировать обновления, использовать DOM и решать другие подобные задачи.
Дэн Абрамов подготовил отличную схему, которая иллюстрирует особенности работы механизмов жизненного цикла компонентов.
Жизненный цикл React-компонентов
Я полагаю, что представление компонентов в виде долгоживущих классов — это не самая лучшая ментальная модель React. Помните о том, что состояние React-компонентов не должно мутировать. Устаревшее состояние должно заменяться на новое. Каждая такая замена вызывает повторный рендеринг компонента. Это даёт React его, пожалуй, самую главную и самую ценную возможность: поддержку детерминированного подхода к созданию визуальных представлений компонентов.
Подобное поведение лучше всего представить себе так: при каждом рендеринге компонента библиотека вызывает детерминированную функцию, возвращающую JSX. Эта функция не должна самостоятельно вызывать собственные побочные эффекты. Но она, если ей это нужно, может передавать React запросы на выполнение подобных эффектов.
Другими словами, большинство React-компонентов имеет смысл представлять себе в виде чистых функций, получающих входные параметры и возвращающих JSX. Чистые функции обладают следующими особенностями:
- Получая одни и те же входные данные, они всегда возвращают одни и те же выходные данные (они являются детерминированными).
- У них нет побочных эффектов (то есть — они не работают с сетевыми ресурсами, не выводят что-либо в консоль, не записывают ничего в
localStorage
и так далее).
Обратите внимание на то, что если для работы некоего компонента нужны побочные эффекты, выполнять их можно, пользуясь useEffect
или обращаясь к создателю действия, переданному компоненту через входные параметры и позволяющему организовать обработку побочных эффектов за пределами компонента.
Хуки React
В React 16.8 появилась новая концепция — хуки React. Это — функции, которые позволяют подключаться к событиям жизненного цикла компонентов, не пользуясь при этом синтаксисом классов и не обращаясь к методам жизненного цикла компонентов. Компоненты, в результате, стало возможным создавать не в виде классов, а в виде функций.
Вызов хука, в целом, означает появление побочного эффекта — такого, который позволяет компоненту работать со своим состоянием и с подсистемой ввода-вывода. Побочный эффект — это любое изменение состояния, видимое за пределами функции, за исключением изменения значения, возвращаемого функцией.
Хук useEffect позволяет ставить побочные эффекты в очередь для их последующего выполнения. Они будут вызываться в подходящее время жизненного цикла компонента. Это время может настать сразу после монтирования компонента (например — при вызове метода жизненного цикла componentDidMount), во время фазы Commit (метод componentDidUpdate), непосредственно перед размонтированием компонента (componentWillUnmount).
Обратили внимание на то, что с одним хуком связано целых три метода жизненного цикла компонента? Дело тут в том, что хуки позволяют объединять связанную логику, а не «раскладывать» её, как было до них, по разным методам жизненного цикла компонента.
Многим компонентам нужно выполнять какие-то действия во время их монтирования, нужно что-то обновлять при каждой перерисовке компонента, нужно освобождать ресурсы сразу перед размонтированием компонента для предотвращения утечек памяти. Благодаря использованию useEffect
все эти задачи можно решить в одной функции, не разделяя их решение на 3 разных метода, не смешивая их код с кодом других задач, не связанных с ними, но тоже нуждающихся в этих методах.
Вот что дают нам хуки React:
- Они позволяют создавать компоненты, представленные в виде функций, а не в виде классов.
- Они помогают лучше организовывать код.
- Благодаря им упрощается совместное использование одной и той же логики в разных компонентах.
- Новые хуки можно создавать, выполняя композицию существующих хуков (вызывая их из других хуков).
В целом, рекомендуется пользоваться функциональными компонентам и хуками, а не компонентами, основанными на классах. Функциональные компоненты обычно компактнее компонентов, основанных на классах. Их код лучше организован, отличается лучшей читабельностью, лучше подходит для многократного использования, его легче тестировать.
Компоненты-контейнеры и презентационные компоненты
Я, стремясь улучшить модульность компонентов и их пригодность для многократного использования, ориентируюсь на разработку компонентов двух видов:
- Компоненты-контейнеры — это компоненты, которые подключены к источникам данных и могут иметь побочные эффекты.
- Презентационные компоненты — это, по большей части, чистые компоненты, которые, получая на вход одни и те же входные параметры и контекст, всегда выдают один и тот же JSX.
Чистые компоненты не следует отождествлять с базовым классом React.PureComponent, который назван именно так из-за того, что его небезопасно использовать для создания компонентов, не являющихся чистыми.
▍Презентационные компоненты
Рассмотрим особенности презентационных компонентов:
- Они не взаимодействуют с сетевыми ресурсами.
- Они не сохраняют данные в
localStorage
и не загружают их оттуда. - Они не выдают неких непредсказуемых данных.
- Они не обращаются напрямую к текущему системному времени (например, путём вызова метода
Date.now()
). - Они не взаимодействуют напрямую с хранилищем состояния приложения.
- Они могут использовать локальное состояние компонента для хранения чего-то наподобие данных, введённых в формы, но при этом они должны поддерживать возможность приёма таких данных и задания на их основе своего исходного состояния, что облегчает их тестирование.
Именно из-за последнего пункта этого списка я, говоря о презентационных компонентах, упомянул о том, что это, по большей части, чистые компоненты. Эти компоненты считывают своё состояние из глобального состояния React. Поэтому хуки вроде useState
и useReducer
дают в их распоряжение неявным образом определённые данные (то есть — данные, не описанные в сигнатуре функции), что, с технической точки зрения, не позволяет назвать такие компоненты «чистыми». Если нужно, чтобы они были бы по-настоящему чистыми, можно делегировать все задачи по управлению состоянием компоненту-контейнеру, но я полагаю, что делать этого не стоит, по крайней мере, до тех пор, пока правильность работы компонента можно проверить с помощью модульных тестов.
Лучшее — враг хорошего.
Вольтер
▍Компоненты-контейнеры
Компоненты-контейнеры — это такие компоненты, которые отвечают за управление состоянием, за выполнение операций ввода-вывода и за решение любых других задач, которые можно отнести к побочным эффектам. Они не должны самостоятельно рендерить некую разметку. Вместо этого они делегируют задачу рендеринга презентационным компонентам, а сами служат обёрткой для таких компонентов. Обычно компонент-контейнер в React+Redux-приложении просто вызывает mapStateToProps()
и mapDispatchToProps()
, после чего передаёт соответствующие данные презентационным компонентам. Контейнеры, кроме того, могут использоваться для решения некоторых задач общего характера, о которых мы поговорим ниже.
Компоненты высшего порядка
Компонент высшего порядка (Higher Order Component, HOC) — это компонент, который принимает другие компоненты и возвращает новый компонент, реализующий новый функционал, основанный на исходных компонентах.
Компоненты высшего порядка функционируют, оборачивая одни компоненты другими. Компонент-обёртка может реализовывать некую логику и создавать элементы DOM. Он может передавать оборачиваемому компоненту дополнительные входные параметры, а может этого и не делать.
В отличие от хуков React и от механизма render props, компоненты высшего порядка поддаются композиции с использованием стандартного подхода к композиции функций. Это позволяет декларативно описывать результаты композиции возможностей, предназначенных для использования в разных местах приложения. При этом готовые компоненты не должны знать о существовании тех или иных возможностей. Вот пример HOC с EricElliottJS.com:
import { compose } from 'lodash/fp';
import withFeatures from './with-features';
import withEnv from './with-env';
import withLoader from './with-loader';
import withCoupon from './with-coupon';
import withLayout from './with-layout';
import withAuth from './with-auth';
import { withRouter } from 'next/router';
import withMagicLink from '../features/ethereum-authentication/with-magic-link';
export default compose(
withEnv,
withAuth,
withLoader,
withLayout({ showFooter: true }),
withFeatures,
withRouter,
withCoupon,
withMagicLink,
);
Тут показана смесь множества возможностей, совместно используемых всеми страницами сайта. А именно, withEnv
читает настройки из переменных окружения, withAuth
реализует механизм GitHub-аутентификации, withLoader
показывает анимацию во время загрузки данных пользователя, withLayout({ showFooter: true })
выводит стандартный макет с «подвалом», withFeature
показывает настройки, withRouter
загружает маршрутизатор, withCoupon
отвечает за работу с купонами, а withMagicLing
поддерживает аутентификацию пользователей без пароля с использованием Magic.
Кстати, учитывая то, что аутентификация пользователей с помощью пароля устарела, и то, что это — опасная практика, в наши дни стоит использовать другие методы аутентификации пользователей.
Почти все страницы вышеупомянутого сайта используют все эти возможности. Учитывая то, что их композиция выполнена средствами компонента высшего порядка, можно включить их все в компонент-контейнер, написав всего одну строку кода. Вот, например, как это будет выглядеть для страницы с уроками:
import LessonPage from '../features/lesson-pages/lesson-page.js';
import pageHOC from '../hocs/page-hoc.js';
export default pageHOC(LessonPage);
У подобных компонентов высшего порядка есть альтернатива, но она представляет собой сомнительную конструкцию, называемую «pyramid of doom» («пирамида погибели») и ей лучше не пользоваться. Вот как это выглядит:
import FeatureProvider from '../providers/feature-provider';
import EnvProvider from '../providers/env-provider';
import LoaderProvider from '../providers/loader-provider';
import CouponProvider from '../providers/coupon-provider';
import LayoutProvider from '../providers/layout-provider';
import AuthProvider from '../providers/auth-provider';
import RouterProvider from '../providers/RouterProvider';
import MagicLinkProvider from '../providers/magic-link-provider';
import PageComponent from './page-container';
const WrappedComponent = (...props) => (
);
И это придётся повторить на каждой странице. А если надо будет что-то в этой конструкции изменить, то изменения в неё придётся вносить везде, где она присутствует. Полагаю, недостатки такого подхода совершенно очевидны.
Использование композиции для решения задач общего характера — это один из лучших способов уменьшения сложности кода приложений. Композиция — это настолько важно, что я даже написал об этом книгу.
Итоги
- Почему React? React даёт нам детерминированный рендеринг визуальных представлений компонентов, в основе которого лежит однонаправленная привязка данных и иммутабельное состояние компонентов.
- JSX даёт нам возможность простого декларативного описания интерфейсов в JavaScript-коде.
- Синтетические события сглаживают кросс-платформенные различия систем обработки событий и облегчают управление памятью.
- Концепция жизненного цикла компонентов направлена на защиту состояния компонентов. Жизненный цикл компонента состоит из фаз монтирования, обновления и размонтирования. Фаза обновления состоит из фазы рендеринга, фазы подготовки к внесению изменений в DOM и фазы внесения изменений в DOM.
- Хуки React позволяют подключаться к методам жизненного цикла компонентов без использования синтаксиса, основанного на классах. Применение хуков, кроме того, облегчает совместное использование одного и того же кода в разных компонентах.
- Компоненты-контейнеры и презентационные компоненты позволяют отделить задачи формирования визуального представления интерфейсов от задач по управлению состоянием приложения и от побочных эффектов. Это улучшает возможности по многократному использованию и тестированию компонентов и бизнес-логики приложения.
- Компоненты высшего порядка упрощают совместное использование возможностей, представляющих собой композицию других возможностей. При этом компонентам не нужно знать об этих возможностях (и не нужно, чтобы компоненты были бы тесно связаны с ними).
Что дальше?
В этом материале о React мы затронули множество концепций функционального программирования. Если вы стремитесь к глубокому пониманию принципов разработки React-приложений, вам полезно будет освежить свои знания о чистых функциях, об иммутабельности, о каррировании и частичном применении функций, о композиции функций. Соответствующие материалы вы можете найти на EricElliottJS.com.
Я рекомендую использовать React совместно с Redux, Redux-Saga и RITEway. Redux рекомендуется использовать совместно с Autodux и Immer. Для организации сложных схем работы с состоянием можно попробовать воспользоваться Redux-DSM.
Когда вы разберётесь с основами и будете готовы к созданию реальных React-приложений, обратите внимание на Next.js и Vercel. Эти инструменты помогут автоматизировать настройку системы сборки проекта и CI/CD-конвейера, с их помощью можно подготовить проект к оптимизированному развёртыванию на сервере. Они дают тот же эффект, что и целая команда DevOps-специалистов, но пользоваться ими можно совершенно бесплатно.
Какие вспомогательные инструменты вы применяете при разработке React-приложений?