Плюсы / минусы использования redux-saga с генераторами ES6 vs redux-thunk с ES2017 async/await

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

прежде чем я обернуть мою голову вокруг него, я хотел бы знать плюсы/минусы использования redux-saga вместо подхода ниже, где я использую 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);

тогда мои действия выглядят что-то вроде этого:--8-->

// 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...

7 ответов


в redux-saga эквивалентом приведенного выше примера будет

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 не имеют никакой контроль на когда остановить обработки этих действий.

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

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

  • дескриптор выхода из системы действие пользователя

  • при первом успешном входе в систему сервер возвращает токен, срок действия которого истекает с некоторой задержкой, хранящейся в


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

Pro (с помощью saga):

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

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

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

Con:

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

  • много понятия для изучения.

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


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

саги велики, чтобы проверить:

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

саг больше мощный. Все, что вы можете сделать в создателе действия one thunk, вы также можете сделать в одной саге, но не наоборот (или, по крайней мере, не легко). Например:

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

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

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

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


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

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

Я видел пара проектов, где thunks были обработаны, как если бы они были контроллерами из MVC patten, и это быстро становится неразборчивым беспорядком.

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


вот проект, который сочетает в себе лучшие части (плюсы) как 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)
    })
  }
}

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

от documantasion

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

  1. нет необходимости в другом промежуточном по Redux async. например, thunk, promise-middleware, saga
  2. легко позволяет передать обещание в redux и это удалось для вас
  3. позволяет совместно находить внешние вызовы службы с тем, где они будут преобразованы
  4. именование файла " init.js " вызовет его один раз при запуске приложения. Это хорошо для загрузки данных с сервера при старте

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

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


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

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

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