Плюсы/минусы использования редукс-саги с генераторами ES6 по сравнению с редукцией с ES2017 async/wait

Сейчас много говорят о последнем мальчике в городе Редукс, Реду-Сага/Редукс-Сага. Он использует функции генератора для прослушивания/диспетчеризации действий.

Прежде чем я обдумаю это, я хотел бы узнать плюсы/минусы использования redux-saga вместо подхода, redux-thunk ниже, где я использую redux-thunk с async/await.

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

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

Тогда мои действия выглядят примерно так:

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// define constants
// define initial state
// export default reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// more actions...

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get('/users/${uid}');
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// more actions...

Ответ 1

В редукционной саге эквивалент приведенного выше примера будет

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

Первое, что нужно заметить, это то, что мы вызываем функции api, используя форму yield call(func, ...args). call не выполняет эффект, он просто создает простой объект, например {type: 'CALL', func, args}. Исполнение делегируется промежуточному программному обеспечению redux-saga, которое заботится о выполнении функции и возобновлении генератора с его результатом.

Главное преимущество заключается в том, что вы можете протестировать генератор вне Redux с помощью простых проверок равенства

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

Обратите внимание, что мы издеваемся над результатом вызова api, просто вводя насмешливые данные в метод next итератора. Измерительные данные проще, чем насмехающиеся функции.

Второе, что нужно заметить, это вызов yield take(ACTION). Thunks вызывается создателем действия для каждого нового действия (например, LOGIN_REQUEST). т.е. действия постоянно подталкиваются к thunks, и thunks не имеют контроля над тем, когда прекращать обработку этих действий.

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

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

  • Действия пользователя LOGOUT

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

  • Учтите, что при ожидании результата вызовов api (начального входа или обновления) пользователь может выйти из системы.

Как бы вы реализовали это с помощью thunks; а также обеспечение полного охвата тестированием всего потока? Вот как это может выглядеть с Сагами:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

В приведенном выше примере мы выражаем наше требование concurrency, используя race. Если take(LOGOUT) выигрывает гонку (т.е. Пользователь нажал кнопку выхода). Гонка автоматически отменит фоновая задача authAndRefreshTokenOnExpiry. И если authAndRefreshTokenOnExpiry был заблокирован в середине вызова call(authorize, {token}), он также будет отменен. Отмена распространяется автоматически вниз.

Вы можете найти runnable demo вышеупомянутого потока

Ответ 2

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

Pro (использование саги):

  • Тестируемость

    . Это очень легко проверить саги, так как call() возвращает чистый объект. Тестирование thunks обычно требует, чтобы вы включили mockStore в свой тест.

  • redux-saga поставляется с множеством полезных вспомогательных функций о задачах. Мне кажется, что концепция саги заключается в том, чтобы создать какой-то фоновый рабочий/нить для вашего приложения, которые действуют как недостающая часть в архитектуре реакции редукции (actionCreators и редукторы должны быть чистыми функциями.) Это приводит к следующей точке.

  • Sagas предлагает независимое место для обработки всех побочных эффектов. В моем опыте обычно легче модифицировать и управлять, чем действия thunk.

Con:

  • Синтаксис генератора.

  • Множество понятий для изучения.

  • стабильность API. Кажется, редукс-сага все еще добавляет функции (например, каналы?), И сообщество не так велико. Существует проблема, если библиотека однажды сделает обновление, не поддерживающее обратную совместимость.

Ответ 3

Я просто хотел бы добавить некоторые комментарии из моего личного опыта (используя как sagas, так и thunk):

Саги отлично подходят для тестирования:

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

Саги более могущественны. Все, что вы можете сделать в одном создателе Thunk Action, вы также можете сделать в одной саге, но не наоборот (или, по крайней мере, нелегко). Например:

  • ждать действия/действий, которые будут отправлены (take)
  • отменить существующую процедуру (cancel, takeLatest, race)
  • несколько подпрограмм могут прослушивать одно и то же действие (take, takeEvery ,...)

Sagas также предлагает другие полезные функции, которые обобщают некоторые общие шаблоны приложений:

  • channels для прослушивания на внешних источниках событий (например, веб-сокеты)
  • модель вилки (fork, spawn)
  • дроссель
  • ...

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

Ответ 4

Здесь проект, который объединяет лучшие части (плюсы) как redux-saga, так и redux-thunk: вы можете обрабатывать все побочные эффекты на сагах, получая обещание от dispatching соответствующего действия: https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}

Ответ 5

Рассмотрев несколько различных крупномасштабных проектов React/Redux на своем опыте, Sagas предоставляет разработчикам более структурированный способ написания кода, который намного проще тестировать и труднее ошибиться.

Да, это немного странно для начала, но большинство разработчиков получают достаточно понимания за один день. Я всегда говорю людям, чтобы они не беспокоились о том, с чего начинается yield, и что когда вы напишете пару тестов, это придет к вам.

Я видел пару проектов, в которых thunks рассматривались так, как будто они являются контроллерами из паттерна MVC, и это быстро становится неразрешимым беспорядком.

Мой совет - использовать Sagas там, где вам нужны триггеры типа B, относящиеся к одному событию. Я считаю, что для всего, что может затрагивать несколько действий, проще написать клиентское промежуточное ПО и использовать мета-свойство действия FSA для его запуска.

Ответ 6

Просто личный опыт:

  1. Что касается стиля кодирования и читабельности, одно из наиболее значительных преимуществ использования redux-saga в прошлом состоит в том, чтобы избежать ада обратного вызова в redux-thunk - больше не нужно использовать много вложений затем /catch. Но теперь, с популярностью асинхронного/ожидающего thunk, можно также написать асинхронный код в стиле синхронизации при использовании redux-thunk, что может быть расценено как улучшение избыточного мышления.

  2. Может потребоваться написать намного больше стандартного кода при использовании redux-saga, особенно в Typescript. Например, если кто-то хочет реализовать асинхронную функцию выборки, обработка данных и ошибок может быть выполнена непосредственно в одном модуле thank в action.js с одним единственным действием FETCH. Но в redux-saga, возможно, потребуется определить действия FETCH_START, FETCH_SUCCESS и FETCH_FAILURE и все связанные с ними проверки типов, потому что одна из функций в redux-saga - использовать этот богатый механизм "токенов" для создания эффектов и инструктирования Редукс магазин для легкого тестирования. Конечно, можно написать сагу, не используя эти действия, но это сделало бы ее похожей на Thunk.

  3. С точки зрения структуры файлов, во многих случаях, как представляется, redux-saga является более явным. Можно легко найти асинхронный код в каждом sagas.ts, но в избыточном коде его нужно будет увидеть в действиях.

  4. Простое тестирование может быть еще одной взвешенной функцией в Redux-Saga. Это действительно удобно. Но одна вещь, которую необходимо пояснить, состоит в том, что тест "вызова" redux-saga не будет выполнять фактический вызов API в тестировании, поэтому необходимо будет указать пример результата для шагов, которые могут использовать его после вызова API. Поэтому, прежде чем писать в redux-saga, лучше спланировать сагу и ее соответствующие sagas.spec.ts в деталях.

  5. Redux-saga также предоставляет множество продвинутых функций, таких как параллельное выполнение задач, помощники по параллелизму, такие как takeLatest/takeEvery, fork/spawn, которые намного мощнее, чем thunks.

В заключение, лично я хотел бы сказать: во многих обычных случаях и приложениях от малого до среднего размера, используйте async/await style redux-thunk. Это сэкономит вам множество стандартных кодов/действий /typedefs, и вам не нужно будет переключаться между многими различными sagas.ts и поддерживать определенное дерево саг. Но если вы разрабатываете большое приложение с очень сложной асинхронной логикой и нуждаетесь в таких функциях, как параллелизм/параллельный шаблон или если у вас высокий спрос на тестирование и сопровождение (особенно в разработке на основе тестов), возможно, что при этом вы сможете сэкономить вашу жизнь.,

В любом случае, redux-saga не более сложен и сложен, чем сам redux, и у него нет так называемой крутой кривой обучения, поскольку он имеет очень ограниченные основные концепции и API. Если вы потратите немного времени на изучение редукс-саги, это может принести вам пользу в будущем.

Ответ 7

Более простой способ - использовать redux-auto.

из документационного материала

redux-auto исправила эту асинхронную проблему, просто создав функцию "действие", которая возвращает обещание. Чтобы сопровождать вашу логику действий функции "по умолчанию".

  • Не нужно использовать другое промежуточное ПО Redux async. например thunk, обещание-middleware, сага
  • Легко позволяет вам сдать обещание в сокращение и вам это удалось
  • Позволяет вам совместно находить внешние вызовы службы, где они будут преобразованы.
  • Именование файла "init.js" вызовет его один раз при запуске приложения. Это полезно для загрузки данных с сервера при запуске.

Идея состоит в том, чтобы каждое действие в определенном файле. совместное размещение вызова сервера в файле с функциями редуктора для "ожидающих", "выполненных" и "отклоненных". Это упрощает обработку promises.

Он также автоматически прикрепляет вспомогательный объект (называемый "async" ) к прототипу вашего состояния, позволяя отслеживать в пользовательском интерфейсе, запрошенные переходы.

Ответ 8

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

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

И конечно, генераторы проще тестировать.