[Перевод] Практические рекомендации по разработке крупномасштабных React-приложений. Часть 2: управление состоянием, маршрутизация

Сегодня мы публикуем вторую часть перевода материала, который посвящён разработке крупномасштабных React-приложений. Здесь речь пойдёт об управлении состоянием приложений, о маршрутизации и о разработке интерфейсов.

berkzjayeeo2ap1yvpeeunb3b6e.png

Управление состоянием приложения, интеграция с Redux, организация маршрутизации


Здесь мы поговорим о том, как можно расширить функционал Redux для того, чтобы получить возможность упорядоченно выполнять в приложении сложные операции. Если подобные механизмы реализовать некачественно — они могут нарушить паттерн, используемый при проектировании хранилища.

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

▍Решение задач, которые нельзя решить с помощью чистых функций


Рассмотрим следующий сценарий. Вам предложили поработать над приложением, предназначенным для компании, которая работает на рынке недвижимости. Клиент хочет обзавестись новым, более качественным веб-сайтом. В вашем распоряжении имеется REST API, у вас есть макеты всех страниц, подготовленные с помощью Zapier, вы набросали план приложения. Но тут подкралась здоровенная проблема.

Компания-клиент уже очень давно использует некую систему управления контентом (CMS). Сотрудники компании отлично знают эту систему, поэтому заказчик не хочет переходить на новую CMS только ради того, чтобы облегчить написание новых постов в корпоративный блог. Кроме того, вам нужно ещё и скопировать на новый сайт существующие публикации из блога, а это тоже может вылиться в проблему.

Хорошо то, что CMS, используемая клиентом, имеет удобный API, через который можно получить доступ к публикациям из блога. Но, если вы создали агент для работы с этим API, ситуацию осложняет то, что оно находится на некоем сервере, на котором данные представлены совсем не так, как вам нужно.

Это — пример проблемы, чего-то такого, что может загрязнить код приложения, так как тут приходится включать в проект механизмы для работы с новым API, используемым для загрузки публикаций, сделанных в блоге. Справиться с этой ситуацией можно с помощью redux-saga.
Взгляните на следующую схему. Так выглядит схема взаимодействия нашего приложения и API. Мы загружаем публикации в фоне, используя redux-saga.

56e2b9fdfc5ea809c842a39cfed53bfe.jpg


Схема приложения, в котором используется хранилище Redux и redux-saga

Здесь компонент выполняет отправку действия GET.BLOGS. В приложении используется redux-saga, поэтому этот запрос будет перехвачен. После этого функция-генератор в фоновом режиме загрузит данные из хранилища данных и обновит состояние приложения, поддерживаемое средствами Redux.

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

...

function* fetchPosts(action) {
 if (action.type === WP_POSTS.LIST.REQUESTED) {
   try {
     const response = yield call(wpGet, {
       model: WP_POSTS.MODEL,
       contentType: APPLICATION_JSON,
       query: action.payload.query,
     });
     if (response.error) {
       yield put({
         type: WP_POSTS.LIST.FAILED,
         payload: response.error.response.data.msg,
       });
       return;
     
     yield put({
       type: WP_POSTS.LIST.SUCCESS,
       payload: {
         posts: response.data,
         total: response.headers['x-wp-total'],
         query: action.payload.query,
       },
       view: action.view,
     });
   } catch (e) {
     yield put({ type: WP_POSTS.LIST.FAILED, payload: e.message });
   
 
...


Представленная здесь сага ожидает появления действий типа WP_POSTS.LIST.REQUESTED. Получая такое действие, она загружает данные из API. Она же, после получения данных, отправляет другое действие — WP_POSTS.LIST.SUCCESS. Его обработка приводит к обновлению хранилища с использованием соответствующего редьюсера.

▍Внедрение редьюсеров


При разработке больших приложений невозможно заранее спланировать устройство всех необходимых моделей. Более того, по мере того, как размер приложения увеличивается, использование техники внедрения редьюсеров помогает сэкономить большой объём человеко-часов. Эта техника позволяет разработчикам добавлять в систему новые редьюсеры, не переписывая всё хранилище.

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

Внедрение редьюсеров — это форма разделения кода. Сообщество React-разработчиков с энтузиазмом осваивает эту технологию. Я буду пользоваться этим фрагментом кода для того чтобы продемонстрировать внешний вид и особенности работы механизма внедрения редьюсеров.

Для начала посмотрим на его интеграцию с Redux:

...

const withConnect = connect(
 mapStateToProps,
 mapDispatchToProps,
);

const withReducer = injectReducer({
 key: BLOG_VIEW,
 reducer: blogReducer,
});

class BlogPage extends React.Component {
  ...
}

export default compose(
 withReducer,
 withConnect,
)(BlogPage);


Этот код является частью файла BlogPage.js, который содержит компонент приложения.

Здесь мы, в команде экспорта, применяем не функцию connect, а функцию compose. Это — одна из функций библиотеки Redux, которая позволяет выполнять композицию нескольких функций. Список функций, передаваемых compose, надо читать справа налево или снизу вверх.

Из документации к Redux можно узнать о том, что функция compose позволяет создавать трансформации глубоко вложенных функций. При этом программист освобождается от необходимости использования очень длинных конструкций. Эти конструкции выглядят как строки кода, представляющие собой вызовы одних функций с передачей им в виде аргументов результатов вызова других функций. В документации отмечается, что функцией compose стоит пользоваться с осторожностью.

Самая правая функция в композиции может принимать множество аргументов, но функциям, следующим за ней, может передаваться лишь один аргумент. В итоге, вызывая ту функцию, которая получилась в результате использования compose, мы передаём ей то, что принимает та из исходных функций, которая находится правее всех остальных. Именно поэтому мы передали функции compose функцию withConnect в качестве последнего параметра. В результате функция compose может быть использована точно так же как функция connect.

▍Маршрутизация и Redux


Существует целый ряд инструментов, которые используются для решения задач маршрутизации в приложениях. В этом разделе мы, однако, остановимся на библиотеке react-router-dom. Мы расширим её возможности таким образом, чтобы она могла бы работать с Redux.

Чаще всего маршрутизатор React используют так: корневой компонент заключают в тег BrowserRouter, а дочерние контейнеры оборачивают в метод withRouter() и экспортируют их (вот пример).

При таком подходе дочерний компонент получает, через механизм props, объект history, содержащий некоторые свойства, специфичные для текущей сессии пользователя. В этом объекте имеются и некоторые методы, которые могут быть использованы для управления навигацией.

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


Для решения этой проблемы мы воспользуемся библиотекой connected-react-router, которая позволит наладить маршрутизацию с использованием метода dispatch. Интеграция в проект этой библиотеки потребует выполнить некоторые модификации. В частности — нужно будет создать новый редьюсер, предназначенный специально для маршрутов (это вполне очевидно), а так же — добавить в систему некоторые вспомогательные механизмы.

Новой системой маршрутизации, после завершения её настройки, можно пользоваться посредством Redux. Так, навигация в приложении может быть реализована путём отправки действий.

Для того чтобы воспользоваться возможностями библиотеки connected-react-router в компоненте, мы просто выполняем мэппинг метода dispatch на хранилище, делая это в соответствии с нуждами приложения. Вот пример кода, который демонстрирует использование библиотеки connected-react-router (для того чтобы подобный код заработал, нужно, чтобы и остальные части системы были бы настроены на использование connected-react-router).

import { push } from 'connected-react-router'
...

const mapDispatchToProps = dispatch => ({
  goTo: payload => {
    dispatch(push(payload.path));
  },
});

class DemoComponent extends React.Component {
  render() {
    return (
       {
            this.props.goTo({ path: `/gallery/`});
          
        
      />
    
  
}

...


Здесь метод goTo отправляет действие, которое помещает необходимый URL в стек истории браузера. Ранее был выполнен мэппинг метода goTo на хранилище. Поэтому этот метод передаётся DemoComponent в объекте props.

Динамический пользовательский интерфейс и нужды растущего приложения


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

▍Ленивая загрузка и React.Suspense


Самое приятное в асинхронной природе JavaScript то, что она позволяет задействовать весь потенциал браузера. Пожалуй, истинным благом можно назвать то, что для запуска некоего процесса не нужно ждать окончания выполнения предыдущего задания. Однако разработчики не могут воздействовать на сеть, и на то, с какой скоростью загружаются различные материалы, необходимые для функционирования сайтов.

Сетевые подсистемы обычно воспринимаются как ненадёжные и подверженные ошибкам.

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

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

В некоторых частях фронтенд-приложения может понадобиться показать некие резервные материалы (такие, которые загружаются гораздо быстрее, чем реальные материалы). Это позволит избавить пользователя от созерцания «дёргано» загружающихся страниц или, что ещё хуже, примерно таких значков.

f47123d6ab6e8e85a0d445742e33031c.png


Пользователям лучше ничего подобного не видеть

Технология React Suspense позволяет справляться именно с такими проблемами. Например, она позволяет вывести некий индикатор во время загрузки данных. Хотя это можно сделать и вручную, установив свойство isLoaded в true, использование API Suspense делает код гораздо более чистым.

Здесь можно посмотреть хорошее видео о Suspense, в котором Джаред Палмер знакомит аудиторию с этой технологией и показывает некоторые из её возможностей на примере реального приложения.

Вот как работа приложения выглядит без использования Suspense.

d497bc7344af043e19445fa8299a35d0.gif


Приложение, в котором Suspense не используется

Оснастить компонент поддержкой Suspense гораздо легче, чем, в масштабах приложения, пользоваться isLoaded. Начнём работу с помещения родительского контейнера App в React.StrictMode. Проследим за тем, чтобы среди модулей React, используемых в приложении, не было бы тех, которые считаются устаревшими.

}>
  
  
  


Компоненты, обёрнутые в теги React.Suspense, во время загрузки основного содержимого загружают и выводят то, что указано в свойстве fallback. Нужно стремиться к тому, чтобы компоненты, используемые в свойстве fallback, имели бы минимально возможный объем и были бы устроены как можно более просто.

9b9d1c54ea0a5d7c26ea7e0a7bbdebee.gif


Приложение, в котором используется Suspense

▍Адаптивные компоненты


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

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

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

6c98b38ab2cf0c2670751908581d26d0.png


Элемент для описания автомобиля и элемент для описания трассы

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

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

Программист может избавить себя от необходимости написания фрагментов кода, которые почти полностью повторяют друг друга. Сделать это можно благодаря разработке адаптивных компонентов. Они, в ходе работы, учитывают окружение, в котором были загружены. Рассмотрим поисковую панель некоего приложения.

a273344e57f1ea8aa88269f8dad8e7dd.png


Поисковая панель

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

static propTypes = {
  open: PropTypes.bool.isRequired,
  setOpen: PropTypes.func.isRequired,
  goTo: PropTypes.func.isRequired,
};


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

Ещё одна интересная ситуация, в которой могут найти применение адаптивные компоненты — это механизм разбиения неких материалов на страницы. Навигационная панель может присутствовать на каждой странице приложения. Экземпляры этой панели на каждой из страниц будут практически точно такими же, как на других страницах.

c867eed8d85f120940a321d95fc274f2.png


Панель разбивки материалов на страницы

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

Итоги


Экосистема React в наши дни стала настолько зрелой, что вряд ли у кого-нибудь возникнет нужда в «изобретении велосипеда» на любом этапе разработки приложения. Хотя это играет на руку разработчикам, это приводит к тому, что сложно становится выбрать именно то, что хорошо подходит для каждого конкретного проекта.

Каждый проект уникален в плане его масштабов и функциональности. В разработке React-приложений нет единого подхода или универсальных правил. Поэтому, прежде чем приступить к разработке, важно её правильно спланировать.

При планировании очень легко понять то, какие инструменты прямо-таки созданы для проекта, а какие ему явно не подходят, являясь слишком масштабными для него. Например, приложение, состоящее из 2–3 страниц и выполняющее очень мало запросов к неким API, не нуждается в хранилищах данных, похожих на те, о которых мы говорили. Я готов пойти в этих рассуждениях ещё дальше, и сказать, что в маленьких проектах не нужно использовать Redux.
На этапе планирования приложения, в ходе рисования макетов его страниц, легко увидеть то, что на этих страницах используется много похожих компонентов. Если постараться повторно использовать код подобных компонентов или стремиться к написанию универсальных компонентов — это поможет сэкономить немало времени и сил.

И наконец, мне хотелось бы отметить, что данные — это стержень любого приложения. И React-приложения — не исключение. С ростом масштабов приложения растут и объёмы обрабатываемых данных, появляются дополнительные программные механизмы для работы с ними. Нечто подобное, если приложение было плохо спроектировано, легко может прямо-таки «раздавить» программистов, завалив их сложными и запутанными задачами. Если же, в ходе планирования, заранее были решены вопросы использования хранилищ данных, если заранее был продуман порядок работы действий, редьюсеров, саг, то работать над приложением будет уже гораздо легче.

Уважаемые читатели! Если вам известны какие-нибудь библиотеки или методологии разработки, которые хорошо показывают себя при создании крупномасштабных React-приложений — просим ими поделиться.

b4fnf52x9i3mn80tttdafqtvkfe.jpeg

© Habrahabr.ru