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

- источник: википедия

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

Прежде чем продолжить, вы должны быть хотя бы знакомы с концепцией управления состоянием Redux в приложении React, если нет, я рекомендую вам потратить несколько минут и прочитать отличное официальное руководство или посмотреть бесплатное видеоурок от соавтора Redux - Дэна Абрамова.

Чего ожидать от этого паттерна

  • Четкие обязанности и заботы по каждому из 4 основных модулей
  • Тесты легче писать и поддерживать
  • Упрощение рефакторинга и поиска ошибок благодаря развязке
  • Предсказуемый и однонаправленный поток данных
  • Готов к предварительным требованиям, таким как совместное использование кода между веб-проектами и собственными проектами.
  • Redux Saga проходит испытания многими крупными производственными компаниями.
  • Продолжайте использовать существующие инструменты отладки Redux
  • Огромное активное сообщество

1. Полезно для новых начинаний или существующей кодовой базы.

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

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

2. Проблема смешивания касается

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

Ниже приведен типичный пример того, как реализовать компонент React, который извлекает данные из вызова API при взаимодействии с пользователем.

class UserProfile extends React.PureComponent {
  onClick = async () => {
    try {
      const response = await fetch('https://api.com/user?q=dan')
      // Handle the response
      // ... business logic in UI code?
    } catch (error) {
      // Handle error
      // ...
    }
  }

  render() {
    return <button onClick={this.onPress}>User Profile</button>
  }
}

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

3. 4 основных модуля

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

Модуль пользовательского интерфейса

Основные проблемы:

  • Визуальное представление данных
  • Реагируйте на изменения данных с течением времени с помощью функции connect () HOF в Redux
  • Обработка взаимодействия с пользователем
  • Отправка событий действия Redux

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

class MyComponent extends Component {
  onClick = () => {
    this.props.fetch_user_request()
  }

  render() {
    return <button onClick={this.onClick}>User Profile</button>
  }
}

Модуль "Сага"

Основные проблемы:

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

Подождите секунду, половина списка - это побочные эффекты! И что в них такого особенного, что их нужно изолировать от других кодов?

гремлины могут работать безупречно 20 раз подряд только 21-го числа, а затем снова начинают работать, когда вы пытаетесь их отладить.

- источник: вы отлаживаете в 2:35 утра

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

60-секундный ускоренный курс по Redux-Saga

(Вы можете пропустить это, если уже знакомы с Redux-Saga)

Чтобы понять следующие примеры кода Saga, все, что вам нужно сделать, это помнить следующие моменты:

  • Саги - это функции-генераторы, поэтому после ключевого слова функции стоит звездочка.
  • Ключевое слово yield похоже на ключевое слово await, оно просто ожидает возврата асинхронного кода перед выполнением следующей строки кода, не блокируя поток JS.
  • Сага получает все события действия Redux в системе, но может решить, какое из них для нее важно.

Пример кода саги

Вот как мы могли бы реализовать побочный эффект получения пользователем в саге.

import { takeLatest, call, put } from 'redux-saga/effects'
export default function* watchFetchUserEvent() {
  yield takeLatest('fetch_user_request', fetchUser)
}

function* fetchUser(action) {
  try {
    const response = yield call(fetch, 'https://api.com/user?q=dan')
    yield put(fetch_user_success(response.json()))
  } catch (error) {
    yield put(fetch_user_error(error))
  }
}

Давайте посмотрим, что делает приведенный выше код.

yield takeLatest(‘fetch_user_request’, fetchUser)
В этой строке просто говорится, что нашу сагу интересует только событие fetch_user_request. Получив это событие, он выполнит функцию fetchUser.

const response = yield call(fetch, ‘https://api.com/user?q=dan')
Побочным эффектом является асинхронный fetch вызов сервера

yield put(fetch_user_success(user)) 
// or
yield put(fetch_user_failed(error))

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

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

Модуль Редукторы

То же, что и Redux, тут добавить особо нечего.

Основные проблемы:

  • Подпишитесь на события
  • Обновить состояние приложения с данными о событиях
  • Здесь живет большая часть вашей бизнес-логики

Модуль Redux Store

Опять же стандартный Redux. Ничего не изменилось.

Основные проблемы:

  • Наш единственный источник достоверной информации о состоянии приложения

Несколько слов о событиях и отладке

Следуя XXX_request XXX_success XXX_failed соглашению об именах для наших событий и используя отладчик Redux. Мы получаем информацию о 3 наиболее распространенных вопросах, которые задаем себе при отладке вызова API.

  1. _request Мы звонили? Было ли это вызвано правильными аргументами 🤦‍♂?
  2. _success Успех? Содержит ли событие полезные данные 🤦‍♀?
  3. _failed Не удалось? О чем нам сообщает информация об ошибке ☠️?

Бонус: пример Github

Https://github.com/ywongweb/redux-saga-soc

Ура! Это конец

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

Redux-Saga намного мощнее того, что мы демонстрировали. Он поставляется с множеством очень полезных рецептов, которые решают многие общие проблемы с асинхронностью, такие как отмена задачи, состояние гонки между несколькими побочными эффектами, отклонение, повторная попытка и т. Д. Я настоятельно рекомендую попробовать это в вашем следующем проекте.

Спасибо за чтение, и я надеюсь, что вы найдете это руководство в некоторой степени полезным для вас. Если у вас есть предложения или вопросы, оставьте комментарий ниже. И, как всегда, приветствуются и ценятся :) Удачного кодирования!

Далее:
Часть 1.1 Что тестировать для каждого из 4 модулей
Часть 1.2 Как перенести Redux-Thunks на этот шаблон
Часть 1.3 Совместное использование кода между React Web и React Native