🔥You Gotta Love Frontend in 3 days. Grab your ticket!
×Закрыть

Подходы и технологии в React Redux: делаем все оптимально

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

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

Продукт — это не код

Необходимо понять, что обычно клиенту не нужен код. Ему нужен продукт, который приносит деньги. Поэтому вовсе не обязательно, чтобы у этого продукта был классный код с минимальным техническим долгом. Вы можете выбрать менее популярный stack, не самые модные инструменты. Критически важно лишь одно: полученный продукт должен решать поставленные задачи. И здесь, к слову, необходимо учитывать, в каких условиях работает клиент, например зависимость от сторонних сервисов (Auth0, Twilio и т. д.).

Технический долг как инструмент

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

Я приведу очень простой пример. Представим, что есть некая задача. У нее есть два возможных решения. Первое: мы можем «закостылить» за три дня разработки. Но у нас при этом возникает технический долг, который однозначно через пару месяцев приведет к потерям для клиента. Второй: мы можем разработать классную масштабируемую архитектуру за две недели, но фронтенд заблочен бэкендом. Бэкенд освобождается через неделю. То есть, мы закончим все через три недели. Согласитесь — вариант еще хуже. Безусловно, есть ситуации, когда в нашем распоряжении есть эти три недели. Но поверьте, это большая редкость.

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

Рекомендованные подходы с React

Приступая к любому проекту, желательно еще на стадии планирования определиться, какие методы мы будем использовать. Я против деления методов на хорошие и плохие. Есть просто те, которые подходят или не подходят к решению определенной задачи. По сути, они должны: повышать производительность; уменьшать стоимость, риски; улучшать тестирование и решить множество других задач, с которым сталкиваются девелоперы.

Среди основных подходов, на которые я рекомендую обращать внимание — использование модульной структуры. Я долгое время работал на бэкенде. Потом — fullstack-ром. Мне понятен и близок MVC-подход, где группировка компонентов происходит по типу данных (Model, View, Controller). С другой стороны, когда я перешел полностью во фронтенд и React в частности, то сделал для себя вывод, что группировка по модулям/компонентам — более профитная. Мы можем один компонент перенести в другой конец приложения, и это будет приемлемо. Ни для кого не секрет, что когда проект развивается, папка components разрастается и может стать необъятной при MVC-подходе, когда файлы группируются по назначению.

Также я рекомендую подход разделения на умные и глупые компоненты. То есть всю бизнес-логику перекидываем в 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 библиотек, по большей части, соответствует функционалу препроцессоров.

Перед началом нового проекта

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

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

Выводы

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

Удачи с выбором «правильных» инструментов.

LinkedIn

29 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

HOC и функция высшего порядка (HOF) это не одно и тоже )))

Higher-order function HOF — обернутая функция в другую функцию-функции.
Higher-order component HOC — обернутый компонет.

Лично мне нравится duck-подход. Утиная типизация. Ее смысл можно передать английским выражением: «If it walks like a duck and it quacks like a duck, then it must be a duck». («Если нечто выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, утка и есть»).

Як повязані redux-ducks і качина типізація в JS ?

Create React App хорошо пока не надо SSR/SEO, более разумно сейчас использовать Next.js или Electrode, поддерживающие и SSR и code-splitting без дополнительных настроек.

Тема Redux не раскрыта, особенно в свете свежевыпущенного React Context API и идейной конкуренции с GraphQL (который реально дерево) и Falcor (который реально граф).

Single Source of Truth хорошо, но этот подход очень плохо работает с SSR + code-splitting, когда и состояние и код прилождения подгружаются частями, по необходимости. Для себе я эту проблему решил, сделав библиотеку github.com/dogada/fast-redux, которая тоже не идеальна, но работает с code-splitting и убирает Redux boilerplate на корню.

Окреме дякую за «технический долг»

styled-components — для стилей, react-router-redux 5 для react-router 4, redux-saga, redux-saga-routines, redux-form ... и будет вам счастье.

с redux-form счастливым не будешь

Порекомендовал бы ознакомиться с react-symbiote как с генератором идей для ваших проектов. Мы сделали свой собственный под TS, с поддержкой опять же кастомной асинхронной redux валидации, рутин и генератором обработчиков жизненного цикла рутины, обработкой акшенов других модулей...
Вместе с сагами — это все, что теперь находится в папке store. Жизнь упрощает очень сильно. Количество бойлерплейта с редаксом снизилось раз в пять.

Мы тоже боремся с бойлерплейтом, только для асинхронных запросов и с нормализацией.
Используем эту либу: www.npmjs.com/...​ckage/redux-actions-async

Спасибо.
Я правильно понял, что єта библиотека требует столько обьектов первого уровня в store, сколько, собственно, их будет использоваться?
Сейчас принято, чтобы в store в первом уровне располагались обьекты, которыми пользуются конкретные модули. Єто упрощает отладку, изолирует модули и прочее.
В todo примере все выглядит просто, а в реальном приложении все очень быстро превращается в кашу.
Кроме того, вижу, что количество кода с єтой библиотекой не уменьшится.
Вот наш пример
symbiote.ts

import { initialState } from '../model';

export interface StatementsSymbioteActions {
  statementListFetch: SymbioteApiListRoutine;
  dummyAction(id: number) : Action
}

@symbiote('statements', initialState)
export class StatementsSymbiote extends Symbiote<StatementsSymbioteActions, any> {
  @routine
  statementListFetch(
    @request(store.list.request(['statements', 'list'])) request?,
    @success(store.list.success(['statements', 'list'])) success?,
    @failure(store.list.failure(['statements', 'list'])) failure?,
    @fulfill(store.list.fulfill(['statements', 'list'])) fulfill?
    ) { }

  @action
  dumyAction(store) => store.setIn([], something);

  @silent('admin')
  somethingFromAdminModule(store, args) {}
}

export const { actions, reducer } = new StatementsSymbiote();
reducer генерируется автоматически на основании атрибутов методов симбиот-класса.
action совместимы с redux-actions
Никаких тонн констант и разбросанных по разным файлам деклараций.
Еще не до конца решили, как обьявлять обработчики request, success.... Поєтому пока так.

saga.ts

const api = new ApiStatements();

export default function* rootSaga() {
  yield all([
    takeEvery(actions.statementListFetch.trigger, statementListFetch)
  ]);
}

function* statementListFetch(links, kind, params) {
  yield call(genericPaginatedItemListDataFetchSaga, actions.statementListFetch, api.fetchStatements, links, kind, params);
}
(тут у нас разбиение на страницы через заголовки серверного респонса, поєтому links и kind параметры)
Все открыто, хелперы используются там, где єто не скрывает логику обработки. Нет места для непонятных ошибок и магии. В любое время почти все куски кода можно развернуть до оригинального состояния. Всегда можно все перекрыть иной, специфической реализацией, и она будет в ожидаемом месте.

Если говорить об оригинальном redux-symbiote, то варианте чистого джаваскрипта, количество кода еще меньше раза в два

1) Вы можете использовать эту библиотеку где угодно в сторе, можете даже какие-то куски написать вручную и скомбинировать их со сгенерированными.
2) Я обычно стараюсь сделать стор максимально плоским: модуль — операция. Большая вложенность обычно приводит к тяжеловестным операциям изменения стора. Но опять же, каждый может компоновать как хочет.
3) Эта библиотека решает только одну проблему: избавление от шаблонного кода для асинхронных экшенов. У вас же я так понимаю более комплексное решение, некоторая своя парадигма для редакса.
4) Количество кода уменьшается очень сильно за счет того, что избавляемся от написания редьюсеров вручную, избавляемся от возможности выстрелить в ногу с мутабельностью (особенно актуально когда на проекте достаточное количество джуниоров), нет необходимости тестировать то, что сгенерировано.

на счет вашей библиотеки:
1) я так понимаю это что-то типа mobx-state-tree только в экосистеме редакса ?
2) хотелось бы посмотреть на комплексный пример для асинхронных экшенов (запрос на сервер, например). так как обычно на проекте есть какой-то стандартный путь обработки ошибок и запросов, который потом все копируют в свои модули.
3) в целом выглядит интересно, но похоже что достаточно большой порог вхождения в бибилотеку

Разумные пояснения. Спасибо. С джунами верно подметили.
В примере выше как раз ситуация отправки запроса за списком платежек и обработки ответа с размещением в сторе с использованием redux-routine, redux-saga, axios, redux-actions, но при этом с минимальным количеством low level кода (однако, когда он нужен, он может быть легко добавлен, и, надеюсь, ничего не сломается)
Если будут нужны детали, напишу в личку.

Спасибо, но какая техническая ценность у статьи? То что redux самая популярная либа реализующая основы flux архитектуры ? или что есть такие вещи как HOC или что есть такая штука как JSS. Информацию об инструментах можно найти на их github страничке, а вот реальных советов как сделать «оптимально» на что претендует заголовок я, увы, не нашел.
Может заголовок не соответствует содержимому, тогда все понятно. Ну в любом случае новичкам будет полезно

Мне кажется, под оптимальностью имелся в виду «оптимальный» подход выполнения проектов в условиях ограниченного времени и денег по методологии «х..к, х..к и в продакшн»

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

Думаю все сталкивались с этим.

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

Поэтому всё что лично я увидел в статье:
— главное не писать хороший код, а уложиться в сроки и бюждет, чтобы удовлетворить клиента (вариант переговоров и убеждения клиента не затронут вообще)
— надо зарабатывать деньги, поэтому, если клиенту нужно срочно, мы напишем костыли с надеждой затем продать часы на рефакторинг этих костылей
— мы используем Redux, чтобы было легче менять гребцов
— вот список инструментов, которые и так все знают
— вывод: писать плохой код, чтобы удовлетворить клиента — нормально

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

спасибо, интересно но мало

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

Любопытная статья. Я бы попробовал сделать фокус именно на экосистеме react-redux, игнорируя остальное.

Например есть отличный курс по шаблонам компонентов реакт. Отлично описаны compound components, render functions, HOC.
egghead.io/...​-react-component-patterns

Набирает популярность шаблон «store-aware» компонентов, называемый Independently Connected Components. Можно загуглить.

Давно заметил, что не хватает книги типа GoF для react-redux. Хотя с учетом скорости изменений, книга не самый удачный формат для этого.

Простенькое замечание: <Component ...props /> не будет работать. Нужно использовать <Component {...props} />

Давно заметил, что не хватает книги типа GoF для react-redux

Так есть же: vasanthk.gitbooks.io/react-bits

Спасибо, не встречал эту книгу. Многих шаблонов жаль не хватает

Простенькое замечание: не будет работать. Нужно использовать

Спасибо, исправил опечатку. Не привычно без IDE )

Хорошая статья, спасибо!
Согласен с большинством утверждений.
Ощущения от Create React App двойственные — с одной стороны много удобных вещей, с другой не всё нравится. Наверное хороший вариант это создание собственного Boilerplate. Минусом будет то, что его придется поддерживать в актуальном состоянии.

вовсе не обязательно, чтобы у этого продукта был классный код

На этом моменте задумался, читать ли дальше

А у вас в проекте идеальный код?

«Сперва добейся», да?)

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

Надеюсь доходчиво объяснил.

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

Я понимаю, как оно бывает в реальном мире. Трейд-офф понятен и для бизнеса это ок. Просто не совпало у меня ожидание от заголовка статьи с её содержимым.

Вы же понимаете, что мы обсуждаем моё субъективное мнение о статье?))

Спасибо что объяснили мне.

вовсе не обязательно, чтобы у этого продукта был классный код
На этом моменте задумался, читать ли дальше

Рано значит еще.

Подписаться на комментарии