Глобальные шаблоны pub sub не новы в разработке программного обеспечения, но Redux быстро вышел на сцену переднего плана. Это неудивительно, учитывая его элегантную реализацию, удобные привязки React и, конечно, то, что это делают все.

Последние семь месяцев я работал над проектом с использованием React / Redux. Я работаю со многими действительно умными людьми, и все же мы часто задаемся вопросом: «Каков правильный путь к _____?»

В результате (и поскольку мы идем по графику) мы реорганизовали наши модули Redux три раза.

Это история этого прогресса, а не только руководство, надеюсь, это поможет кому-то еще быстрее приблизиться к RightWay ™.

Я предполагаю, что вы уже создали несколько вещей с помощью Redux или, по крайней мере, хоть немного разбираетесь в этом.

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Примеры кода краткие, вероятно, содержат ошибки, а обсуждаемые идеи можно справедливо назвать мнениями.

Первый: преобразование действий в потоки

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

Мы знаем, что когда вы извлекаете данные или выполняете ввод-вывод с побочными эффектами, вы должны обрабатывать это с помощью «преобразователя», «саги», «эпоса», «логики», «эффектов» или ( вставьте здесь любое слово, которое означает то же самое). Без проблем.

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

Эти две идеи изменили наше представление о действиях:

  1. Действия с побочными эффектами (в традиционном смысле), независимо от вашего выбора метода, по сути, являются потоком событий. Действия на входе, действия на выходе. Да! Это делает RxJS очень прекрасным способом для обработки действий.
  2. Что еще более важно, некоторые (но не все) действия без легко воспринимаемых «эффектов» могут (и должны) также рассматриваться как потоки.

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

...
case SELECT_COLOR:
case SELECT_SIZE:
case SELECT_MATERIAL:
  return {
    ...state,
    ...{ inStockMessage: action.payload.message } 
  };
...

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

const selectColor = createAction(SELECT_COLOR, (color) => {
  const message = checkStockMessagesForColor(color); 
  return { color, message };
};

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

case IN_STOCK_MESSAGE_CHANGED:
  return {
    ...state,
    ...{ inStockMessage: action.payload.message } 
  };

Поэтому мы изменили наш редуктор, чтобы обновлять состояние на основе одного события, а не складывать их вместе. Вы также заметили, как мы изменили время действия? Отлично. Теперь давайте посмотрим, как мы можем сгенерировать простое нисходящее событие из исходного действия SELECT_COLOR. В этом примере используется redux-observable:

const selectColor = $action => // a stream of all actions
  $action
    .ofType(SELECT_COLOR)  // "filter only this type"
    .flatMap((action) => { // do the things
      const { color } = action.payload;  
      const message = checkStockMessagesForColor(color);
      return Observable.of(inStockChanged(message));
    })
    .switchMap((action) => {
      // do even more things
      return Observable.of(anotherDownstreamEvent());
    })
    .catch(error => Observable.of(handleError(error));

Это может показаться немного корявым для любого, у кого нет доступа к RxJS, но вы можете получить такое же улучшение с помощью простого преобразователя:

const selectColor = color => (dispatch) => {
  const message = checkStockMessagesForColor(color);
  dispatch(inStockChanged(message));
  dispatch(anotherDownstreamEvent());
}

Я бы посоветовал всем узнать больше о Observable / RxJS, так как это потрясающе (хотелось бы, чтобы у меня было больше времени с ним), но реальный вывод таков:

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

Два: переместить логические и производные данные в селекторы

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

В результате переноса логики в эти функции есть несколько преимуществ.

Во-первых, он удаляет логику из компонентов (также известных как представление):
. Обычно каждый согласен, что слой представления должен быть «тупым». или, более описательно, лишенный логики. Мы уделили время тому, чтобы подключенные контейнеры использовали набор функциональных компонентов без сохранения состояния, готовых принять свои свойства как есть. Твердый.

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

...
handleUserClick = (event) => {
  if (this.props.userIsAdmin && this.props.userListIsVisible) { 
    dispatchUserClick();
  }
}
...
render() {
  const { users, searchTerm } = this.props;
  const filteredUsers = users
    .filter(user => 
       user.active && user.name.indexOf(searchTerm) > -1);

  <SimpleUserList 
    users={filteredUsers}
    onClick={this.handleUserClick}
  />
  ...
}

Так в чем же дело? Это просто контейнер. Мы в порядке, правда? Верно?

Что ж ... если у него есть метод рендеринга, вероятно, справедливо считать его видом. Не говоря уже о том, что вся эта логика делает их ОГРОМНОЙ (слишком рано?) Головной болью для тестирования!

Итак, давайте избавимся от этой логики:

...
handleUserClick = (event) => {
  dispatchUserClick();  
}
...
render() {
  <SimpleUserList 
    users={filteredUsers}
    onClick={this.handleUserClick}
  />
...
}

Намного лучше. Итак, куда делась логика? Сначала давайте инкапсулируем его в селекторы с помощью функции createSelector повторного выбора. Начнем с логики фильтрации:

export const filteredUserList = createSelector(
   (state) => state.userList,
   (state) => state.ui,
   (userList, ui) => 
      userList.filter(user => 
       user.active && user.name.indexOf(ui.searchTerm) > -1);
);

Затем мы можем использовать этот селектор в функции контейнера connect:

import { filteredUserList } from '../selectors/userList';
...
const selector = createSelector(
  filteredUserList,
  (users) => { filteredUsers: users }
);
export default connect(selector, dispatcher)(UserList)

Таким образом, мы не только очистили наше представление, но и при повторном выборе запомнили результаты функции, что может ускорить работу, если вы сделаете какую-либо тяжелую работу. Сладкий "маленький бонус".

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

export const userIsAdminAndListVisible = createSelector(
   (state) => state.user,
   (state) => state.ui,
   (user, ui) => user.isAdmin && ui.userListVisible
);

Замечательно, у нас есть логическое значение, производное от состояния, которое сообщает нам, когда что-то должно произойти. Итак, где это, если не в обработчике компонентов? Ты понял! Наш новый и улучшенный поток действий:

import { userIsAdminAndListVisible } from '../selectors/userList';
...
export const userDetailRequest = ($action, store) => 
 $action
 .filter(action => 
    action.type === CLICK_USER || action.type === CLICK_USER_ICON) 
 )
 .flatMap((action) => {
    if (userIsAdminAndListVisible(store.getState())) {  
      // OK to do things! 
    }
 });

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

Несколько дополнительных преимуществ переноса логики в селекторы:

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

Если инкапсулировать каждый бит логики в отдельный селектор кажется излишним, вы абсолютно правы. То, как вы создаете и скомпоновываете эти функции, определенно изменится по мере того, как вы создаете и реорганизуете. Это подводит нас к нашей прощальной мысли:

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

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

Тактичное различие между истинным состоянием (логика редуктора) и данными, которые могут быть получены из состояния (логика селектора) , сделает вашу жизнь проще.

В заключение

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

Я тоже все еще учусь!

Также не было времени написать более короткую статью, извиняюсь за это.
🐧