Подходы и технологии в React Redux: делаем все оптимально
За последние 3 года работы с React я создал с нуля около десятка проектов, как небольших (от месяца самостоятельной разработки), так и довольно объемных (год разработки двумя командами). В своей статье поделюсь опытом выбора подходов и инструментов для старта нового проекта и рефакторинга существующего на React/Redux. Это может быть интересно как новичкам в React, так и более опытным девелоперам.
Когда речь заходит о выборе инструментов для решения задач в React-проектах, то все сводится к главному — оптимальность. Каждый продукт требует особого подхода. Если мы говорим об условно неограниченном бюджете, но ограниченных сроках — это один подход. Если ситуация совершенно обратная — это требует выбора других технологий. Как правило, мы сталкиваемся с тем, что ограничен и бюджет, и сроки. В этом случае наиболее удачный (оптимальный) выбор инструментов и позволит нам получить наиболее качественный результат.
Продукт — это не код
Необходимо понять, что обычно клиенту не нужен код. Ему нужен продукт, который приносит деньги. Поэтому вовсе не обязательно, чтобы у этого продукта был классный код с минимальным техническим долгом. Вы можете выбрать менее популярный stack, не самые модные инструменты. Критически важно лишь одно: полученный продукт должен решать поставленные задачи. И здесь, к слову, необходимо учитывать, в каких условиях работает клиент, например зависимость от сторонних сервисов (Auth0, Twilio и т. д.).
Технический долг как инструмент
Одним из краеугольных камней разработки продукта может стать вышеупомянутый технический долг. С одной стороны, большой технический долг приводит к увеличенным затратам в будущем. С другой стороны, продукт, не выпущенный в срок, не принесет клиенту прибыль. Любой бизнес работает в этих условиях. Но к техническому долгу можно относиться не как к проблеме, а как к инструменту.
Я приведу очень простой пример. Представим, что есть некая задача. У нее есть два возможных решения. Первое: мы можем «закостылить» за три дня разработки. Но у нас при этом возникает технический долг, который однозначно через пару месяцев приведет к потерям для клиента. Второй: мы можем разработать классную масштабируемую архитектуру за две недели, но фронтенд заблочен бэкендом. Бэкенд освобождается через неделю. То есть, мы закончим все через три недели. Согласитесь — вариант еще хуже. Безусловно, есть ситуации, когда в нашем распоряжении есть эти три недели. Но поверьте, это большая редкость.
Патовая ситуация, верно? Для того чтобы избежать ее, есть третье решение — оптимальное. Мы изначально создаем технический долг, а затем приходим к клиенту и продаем идею: у нас есть три дня на разработку, мы вкладываемся в релиз, мы создаем технический долг на будущее, чтобы решить проблему. Смотрим по плану — через два месяца есть время, чтобы выполнить технический долг и все пофиксить. В большинстве случаев, это возможно.
Рекомендованные подходы с React
Приступая к любому проекту, желательно еще на стадии планирования определиться, какие методы мы будем использовать. Я против деления методов на хорошие и плохие. Есть просто те, которые подходят или не подходят к решению определенной задачи. По сути, они должны: повышать производительность; уменьшать стоимость, риски; улучшать тестирование и решить множество других задач, с которым сталкиваются девелоперы.
Среди основных подходов, на которые я рекомендую обращать внимание — использование модульной структуры. Я долгое время работал на бэкенде. Потом — fullstack-ром. Мне понятен и близок
Также я рекомендую подход разделения на умные и глупые компоненты. То есть всю бизнес-логику перекидываем в smart-компоненты (работа с модулями, манипуляции с данными), а dumb-компоненты просто отображают результат. Это позволяет лучше управлять рендером, проводить работу с данными на уровне контейнера. С другой стороны, это логически более читаемо. Если нужно работать с версткой — переходим в компонент, если нужно работать с данными — переходим в контейнер.
Отдельно хочу отметить, что командная разработка и разработка одним девелопером могут сильно отличаться. Если ты один на проекте, полностью знаешь его, то многие вещи можно упразднять. Например, можно прокидывать пропсы spread оператором (<Component {...props} />
). Но работа в команде требует четкого понимания процесса от всех участников. Переопределение тех же атрибутов позволит человеку за соседним столом не подниматься по всему дереву компонентов, а сразу видеть, какие из них передаются (<Component attr1={props.attr1} attr2={props.attr2} />
). Для командной работы также очень актуальна типизация данных, определение PropTypes и defaultProps.
Redux — как инструмент для работы с данными
Помимо работы с компонентами, необходимо понять, где и как будут храниться данные. Например, сейчас я занимаюсь проектом Vantage (Transport Management System) — портал для перевозчиков и заказчиков перевозок. Для работы с данными наша команда использует Redux. Одна из основных причин, почему мы остановили наш выбор на нем — девелоперов с опытом Redux гораздо больше, чем с другим.
Основное преимущество Redux — это управление как состоянием данных, так и состоянием интерфейса. Redux — единственный источник истины при разработке. Все очень упрощается, когда ты знаешь, где искать последнюю и актуальную информацию. Есть множество способов и подходов при работе с Redux. Лично мне нравится duck-подход. Утиная типизация. Ее смысл можно передать английским выражением: «If it walks like a duck and it quacks like a duck, then it must be a duck». («Если нечто выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, утка и есть»).
Концепт этого подхода заключается в том, что мы группируем в одном месте все модули: action creators, actions, reducers. Например, в нашем проекте мы все модули храним с компонентами. Есть подход группировки по constants, actionCreators, actions в отдельных файлах. Я пробовал такой подход в самом начале работы с Redux, и, на мой взгляд, намного проще, когда все сгруппировано именно в компонентах.
Работа с функцией высшего порядка
Еще один подход, который в последнее время набирает популярность, — использование функций высшего порядка (HOC). Это функция, которая может принимать в качестве аргументов другие функции и/или возвращать функции. Поясню на примере.
Допустим, мы создаем компонент BirthdayPresents
, который должен отображать подарки на день рождения. Перед тем, как его вызвать, нам нужно получить данные. Эти данные можно получить в самом компоненте: посылаем запрос на сервер и получаем список людей, а также их подарки. Есть второй способ. Мы можем сначала вызвать функцию withUsers
— получаем список юзеров. Дальше мы вызываем функцию withBirthday
— получаем список пользователей, у которых сегодня день рождения. Дальше мы получаем список подарков с помощью withPresents
. Потом мы можем исключить из списка подарков, скажем, желтые с помощью функции withoutYellow
. Таким образом, перед тем как мы получили наш основной компонент BirthdayPresents
, мы уже получили все данные, просто сформировав цепочку этих данных:
compose( withUsers, withBirthdays, withPresents, withoutYellow )(BirthdayPresents)
Среди минусов — полная зависимость от порядка вызова. Например, если мы поменяем местами withUsers
и withPresents
, то наш HOC не сможет справиться с задачей — так как withPresents не найдет списка юзеров, что может быть обязательным параметром. Кроме того, HOC может сам менять данные. И когда мы столкнемся с такой проблемой, нам нужно будет сначала понять, что у нас с этим есть проблема, а в большинстве случаев это может быть сложно. Для ее решения, нам нужно либо писать новый HOC (такой же как исходный с некоторыми изменениями), либо править уже существующий, что может сломать логику в исправно работающих местах. Это основные подходы, которые мы используем в нашем приложении Vantage.
Установка и настройка сборки проекта
Хотелось бы еще затронуть тему настройки сборки на старте проекта. Есть старый добрый способ: подключаем bower или npm, конфигурируем webpack, настраиваем все сами. Это долго. Как правило, чтобы сократить время, конфигурация с предыдущего проекта просто переносится на новый. И казалось — работает и работает. Но по факту, мы перенесли устаревший код, и это может стать проблемой и ограничением. Поэтому мне больше нравится использование boilerplates. Самый популярный сейчас — Create React App от Facebook. Всего одной командой он конфигурирует все приложение.
Мой фаворит — React Redux Starter Kit, который, к сожалению, уже не поддерживается из-за появления React Router 4. Не поддерживает и plain routes в полном объеме — а они были основой этого boilerplate.
Работа со стилями
Для стилей у нас всегда есть CSS. Но так уже никто не делает. Многие используют препроцессоры SCSS или LESS. Суть в том, что мы работаем с SCSS или LESS синтаксисом, который с помощью webpack преобразовывается в CSS. Препроцессоры позволяют использовать функционал, недоступный в самом CSS, например, переменные, вложенности, наследование и многие другие.
Есть JSS-подход. Мы работаем в JS и постоянно генерируем свои специальные классы. На проекте может быть сотня файлов с классом container. Но JSS каждый раз генерирует новый класс (container-1, container-2...). В таком случае эти стили мы можем хранить в компоненте. Что важно — стиль из одного компонента не может изменить стиль другого компонента без нашего участия. Это позволяет инкапсулировать данные. Функционал JSS библиотек, по большей части, соответствует функционалу препроцессоров.
Перед началом нового проекта
Перед тем, как приступить к новому проекту, я анализирую предыдущий, составляю список проблем, с которыми столкнулась команда. В работе могли возникать проблемы со скоростью загрузки или сборки, при работе с бэкендом, с графикой или стилями, при работе со сторонними библиотеками или текущими архитектурными решениями и многое другое. На основе этих данных я и могу определить, что можно улучшить, а что оптимизировать.
Этот анализ позволяет мне расставить приоритеты для нового проекта. Во многих случаях планирование позволяет сократить и сроки, и трудозатраты. А главное, именно такой способ дает возможность адекватно выбрать нужные подходы и технологии для нового проекта.
Выводы
Еще раз хочу обратить внимание, инструменты существуют для решения задач клиента, а не наоборот. Конечно, компетенции клиента может быть недостаточно для учета всех рисков и понимания технического долга. Одна из наших основных обязанностей, как разработчиков, — экспертная оценка при выборе технологий и подходов для проекта. Не существует хороших и плохих инструментов, есть подходящие и неподходящие в каждом конкретном случае. Правильный выбор может стать как хорошим конкурентным преимуществом, так и головной болью в будущем.
Удачи с выбором «правильных» инструментов.