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

Сейчас много говорят о последнем ребенке в городе редукс, 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);

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

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

person hampusohlsson    schedule 21.01.2016    source источник
comment
См. Также мой ответ, сравнивающий redux-thunk с redux-saga здесь: stackoverflow.com/a/34623840/82609   -  person Sebastien Lorber    schedule 25.01.2016
comment
Что будет :: перед тем, как сделать this.onClick?   -  person Downhillski    schedule 20.07.2016
comment
@ZhenyangHua - это сокращение для привязки функции к объекту (this), иначе this.onClick = this.onClick.bind(this). Более длинную форму обычно рекомендуется делать в конструкторе, так как сокращенная форма повторно привязывается при каждом рендеринге.   -  person hampusohlsson    schedule 20.07.2016
comment
Понятно. Благодарность! Я вижу, что люди часто используют bind(), чтобы передать this функции, но я начал использовать () => method() сейчас.   -  person Downhillski    schedule 20.07.2016
comment
@hampusohlsson Вы нашли сагу о редукции полезной? После объяснения я не нашел никакой пользы.   -  person Hosar    schedule 23.06.2017
comment
@Hosar Некоторое время я использовал redux и redux-saga в производстве, но фактически перешел на MobX через пару месяцев из-за меньших накладных расходов   -  person hampusohlsson    schedule 23.06.2017


Ответы (9)


В 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). Преобразователи вызываются создателем действия для каждого нового действия (например, LOGIN_REQUEST). т.е. действия постоянно передаются в преобразователи, и преобразователи не могут контролировать, когда прекратить обработку этих действий.

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

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

  • Обработка действий пользователя 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) )
    }
  }
}

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

Вы можете найти работающую демонстрацию вышеуказанного потока

person Yassine Elouafi    schedule 21.01.2016
comment
@yassine откуда взялась функция delay? А, нашла: - person philk; 02.03.2016
comment
Код redux-thunk вполне читабелен и не требует пояснений. Но redux-sagas один действительно нечитаем, в основном из-за этих глагольных функций: call, fork, take, _6 _... - person zachguo; 29.06.2016
comment
@syg, я согласен, что call, fork, take и put могут быть более семантически дружественными. Однако именно те глагольные функции делают все побочные эффекты проверяемыми. - person Downhillski; 20.07.2016
comment
FYI, js bin требует обновления: ReferenceError: Redux не определен - person justingordon; 11.08.2016
comment
@justingordon Исправлено, исправлено с 2016 08 11. - person Denialos; 13.09.2017
comment
@syg по-прежнему функция с этими странными глаголами функции более читабельны, чем функция с цепочкой глубоких обещаний - person Yasser Sinjab; 27.11.2017
comment
эти странные глаголы также помогают понять отношение саги к сообщениям, исходящим из редукции. вы можете извлечь типы сообщений из redux - часто для запуска следующей итерации, и вы можете вставить новые сообщения обратно, чтобы транслировать результат вашего побочного эффекта. - person worc; 05.10.2018

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

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

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

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

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

Против:

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

  • Множество концепций, которые нужно изучить.

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

person yjcxy12    schedule 10.06.2016
comment
Просто хочу сделать небольшой комментарий, создатель экшена не должен быть чистой функцией, о чем неоднократно заявлял сам Дэн. - person Marson Mao; 28.04.2017
comment
На данный момент очень рекомендуются redux-sagas, поскольку их использование и сообщество расширились. Кроме того, API стал более зрелым. Рассмотрите возможность удаления Con для API stability в качестве обновления, отражающего текущую ситуацию. - person Denialos; 13.09.2017
comment
saga имеет больше запусков, чем thunk, и ее последняя фиксация тоже после thunk - person amorenew; 28.09.2017
comment
Да, у FWIW redux-saga теперь 12k звезд, у redux-thunk 8k - person Brian Burns; 13.03.2018
comment
Я собираюсь добавить еще одну проблему саг: саги по умолчанию полностью отделены от действий и создателей действий. В то время как Thunks напрямую связывают создателей действий с их побочными эффектами, саги оставляют создателей действий полностью отделенными от саг, которые их слушают. Это имеет технические преимущества, но может значительно усложнить восприятие кода и размыть некоторые однонаправленные концепции. - person theaceofthespade; 29.01.2019

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

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

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

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

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

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

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

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

person madox2    schedule 12.10.2017

Обновление в июле 2020 года:

Возможно, самое заметное изменение в сообществе React за последние 16 месяцев - это перехватчики React.

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

  1. hook + async thunk (hook делает все очень гибким, так что вы действительно можете разместить async thunk там, где вы хотите, и используйте его как обычные функции, например, по-прежнему пишите thunk в action.ts, а затем используйтеDispatch () для запуска thunk: https://stackoverflow.com/a/59991104/5256695),
  2. useRequest,
  3. GraphQL / Apollo useQuery useMutation
  4. react-fetching-library
  5. другие популярные варианты выборки данных / библиотек вызовов API, инструментов, шаблонов проектирования и т. д.

Для сравнения, redux-saga на данный момент не дает значительных преимуществ в большинстве обычных случаев вызовов API по сравнению с вышеупомянутыми подходами, одновременно увеличивая сложность проекта за счет введения множества файлов / генераторов саг (также потому, что последний выпуск v1.1.1 из redux-saga был включен 18 сен 2019, что было очень давно).

Но все же redux-saga предоставляет некоторые уникальные функции, такие как эффект гонок и параллельные запросы. Поэтому, если вам нужны эти специальные функции, redux-saga все равно будет хорошим выбором.


Исходный пост в марте 2019 года:

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

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

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

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

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

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

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

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

person Jonathan    schedule 27.03.2019
comment
Я согласен с вашим обновлением 2020 года, я использую saga в течение 1 года, прежде чем перейти на минималистичную библиотеку api хуков, которая может очень хорошо справляться с побочными эффектами, не добавляя дополнительной сложности. Если интересно: github.com/marcin-piela/react-fetching-library (Я не являюсь автором этой библиотеки) - person Jean Walrave; 07.09.2020
comment
@Jonathan - не могли бы вы привести пример кода того, что вы имеете в виду под крючком + асинхронным преобразователем? - person Joey Baruch; 08.09.2020
comment
Saga по-прежнему «рекомендуется» с учетом React Hooks? Если я нахожусь в затруднительном положении, учитывая этот аргумент, это может быть для меня ключевым фактором ... - person BBaysinger; 02.06.2021
comment
Лично я бы рекомендовал использовать hook + thunk для большинства обычных случаев, но все же было бы лучше проверить sage doc самостоятельно и посмотреть, нужны ли вашему проекту какие-либо из его специальных функций. @BBaysinger - person Jonathan; 02.06.2021
comment
Я выбрал его просто из-за его простоты, тестируемости и того, что он был «рекомендован», но ничего конкретного. Что для вас будет определяющим фактором, если вы не будете использовать его только для этих целей? - person BBaysinger; 03.06.2021
comment
В основном из-за синтаксиса генератора. Когда у нас уже есть async / await повсюду в нашем проекте и async / await также можно использовать для решения всей нашей выборки данных, не будет необходимости вводить другой механизм только для части API. - person Jonathan; 03.06.2021

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

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

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

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

person David Bradshaw    schedule 14.06.2018

Панки против саги

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

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

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

В дополнение к отправке стандартных действий Redux-Thunk промежуточное ПО позволяет отправлять специальные функции, называемые thunks.

Преобразователи (в Redux) обычно имеют следующую структуру:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

То есть thunk - это функция, которая (необязательно) принимает некоторые параметры и возвращает другую функцию. Внутренняя функция принимает dispatch function и getState функцию - обе будут предоставлены Redux-Thunk промежуточным программным обеспечением.

Redux-Saga

Redux-Saga промежуточное ПО позволяет выражать сложную логику приложения в виде чистых функций, называемых сагами. Чистые функции желательны с точки зрения тестирования, потому что они предсказуемы и воспроизводимы, что делает их относительно легкими для тестирования.

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

Функция генератора определяется следующим образом. Обратите внимание на звездочку после ключевого слова функции.

function* mySaga() {
    // ...
}

После регистрации саги о входе в Redux-Saga. Но тогда yield дубль первой строки приостановит сагу до тех пор, пока в магазин не будет отправлено действие с типом 'LOGIN_REQUEST'. Как только это произойдет, выполнение будет продолжено.

Подробнее см. в этой статье.

person Mselmi Ali    schedule 31.07.2019

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

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

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

person Dmitriy    schedule 14.06.2018

Вот проект, который сочетает в себе лучшие части (плюсы) как 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)
    })
  }
}
person Diego Haz    schedule 23.05.2017
comment
использование then() внутри компонента React противоречит парадигме. Вам следует обработать измененное состояние в componentDidUpdate, а не ждать выполнения обещания. - person ; 07.06.2017
comment
@ Maxincredible52 Это неверно для рендеринга на стороне сервера. - person Diego Haz; 07.06.2017
comment
По моему опыту, точка зрения Макса все еще верна для рендеринга на стороне сервера. Это, вероятно, следует обрабатывать где-нибудь на уровне маршрутизации. - person ThinkingInBits; 07.09.2017
comment
@ Maxincredible52, почему это противоречит парадигме, где вы это читали? Я обычно делаю то же самое, что и @Diego Haz, но делаю это в componentDidMount (согласно документации React, сетевые вызовы должны выполняться там предпочтительно), поэтому у нас есть componentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} } - person user3711421; 14.11.2017

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

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

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

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

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

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

person codemeasandwich    schedule 24.06.2017
comment
Я сделал +1, даже если это неуместный ответ, потому что нужно учитывать и другие решения. - person amorenew; 28.09.2017
comment
Я думаю, что это связано с тем, что он не раскрыл, что является автором проекта. - person jreptak; 23.12.2017