Плюсы / минусы использования Redx-Saga с генераторами ES6 против Redx-Thunk с ES2017 Async/ Await
Сейчас много говорят о последнем мальчике в городе Редукс, Реду-Сага / Редукс-Сага. Он использует функции генератора для прослушивания / диспетчеризации действий.
Прежде чем обернуть голову, я хотел бы знать плюсы / минусы использования redux-saga
вместо подхода ниже, где я использую redux-thunk
с асинхронным ожиданием
Компонент может выглядеть следующим образом, диспетчеризировать действия как обычно.
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...
11 ответов
В 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-саге генераторы вытягивают следующее действие. то есть они имеют контроль, когда слушать какие-то действия, а когда нет. В приведенном выше примере инструкции потока размещены внутри while(true)
цикл, поэтому он будет прослушивать каждое поступающее действие, что несколько имитирует поведение толчка.
Тяговый подход позволяет реализовать сложные потоки управления. Предположим, например, что мы хотим добавить следующие требования
Обработка действий пользователя LOGOUT
при первом успешном входе в систему сервер возвращает токен, срок действия которого истекает с некоторой задержкой, хранящейся в
expires_in
поле. Придется обновить авторизацию в фоновом режиме на каждомexpires_in
миллисекундыПримите во внимание, что при ожидании результата вызовов API (либо первоначального входа в систему, либо обновления) пользователь может выйти из промежуточного режима.
Как бы вы реализовали это с помощью Thunk; в то же время обеспечивая полное тестовое покрытие для всего потока? Вот как это может выглядеть с Sagas:
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})
позвоните, это также будет отменено. Отмена распространяется вниз автоматически.
Вы можете найти работающую демонстрацию вышеупомянутого потока
Я добавлю свой опыт использования саги в производственную систему в дополнение к довольно полному ответу автора библиотеки.
Pro (используя сагу):
Тестируемость. Тестировать саги очень легко, так как call() возвращает чистый объект. Тестирование Thunks обычно требует, чтобы вы включили mockStore внутри вашего теста.
Редукс-сага содержит множество полезных вспомогательных функций для выполнения задач. Мне кажется, что идея саги заключается в создании своего рода фонового рабочего процесса / потока для вашего приложения, который выступает в качестве недостающего элемента в архитектуре реагирующего редукса (actionCreators и редукторы должны быть чистыми функциями.) Что приводит к следующему пункту.
Саги предлагают независимое место для обработки всех побочных эффектов. В моем опыте, как правило, легче изменить и чем управлять
Против:
Синтаксис генератора.
Много идей для изучения.
API стабильность. Кажется, что Redx-сага все еще добавляет функции (например, каналы?), И сообщество не такое большое. Существует опасение, если библиотека когда-нибудь сделает несовместимое обновление.
Я просто хотел бы добавить некоторые комментарии из моего личного опыта (с использованием саг и thunk):
Саги отлично подходят для тестирования:
- Вам не нужно имитировать функции, обернутые эффектами
- Поэтому тесты чисты, читабельны и легко пишутся
- При использовании саг создатели действий в основном возвращают обычные литералы объектов. Это также легче проверить и утверждать, в отличие от обещаний Тунка.
Саги более могущественны. Все, что вы можете сделать в создателе действий одного толка, вы также можете сделать в одной саге, но не наоборот (или, по крайней мере, нелегко). Например:
- дождаться отправки действия / действий (
take
) - отменить существующую процедуру (
cancel
,takeLatest
,race
) - несколько подпрограмм могут прослушивать одно и то же действие (
take
,takeEvery
...)
Sagas также предлагает другие полезные функции, которые обобщают некоторые общие шаблоны приложений:
channels
прослушивать внешние источники событий (например, веб-сокеты)- модель вилки (
fork
,spawn
) - дроссель
- ...
Саги отличный и мощный инструмент. Однако с властью приходит ответственность. Когда ваше приложение растет, вы можете легко потеряться, выяснив, кто ожидает отправки действия или что все происходит при отправке какого-либо действия. С другой стороны, это проще и легче рассуждать. Выбор того или другого зависит от многих аспектов, таких как тип и размер проекта, какие побочные эффекты должен обрабатывать ваш проект или предпочтения команды разработчиков. В любом случае просто сделайте ваше приложение простым и предсказуемым.
Просто личный опыт:
Что касается стиля кодирования и читабельности, одно из наиболее значительных преимуществ использования redux-saga в прошлом - это избежать ада обратного вызова в redux-thunk - больше не нужно использовать много вложений then / catch. Но теперь, с популярностью асинхронного / ожидающего thunk, можно также написать асинхронный код в стиле синхронизации при использовании избыточного-thunk, что можно рассматривать как улучшение в избыточном мышлении.
Может потребоваться написать гораздо больше стандартного кода при использовании redux-saga, особенно в Typescript. Например, если кто-то хочет реализовать асинхронную функцию выборки, обработка данных и ошибок может быть выполнена непосредственно в одном модуле thank в action.js с одним единственным действием FETCH. Но в redux-saga, возможно, потребуется определить действия FETCH_START, FETCH_SUCCESS и FETCH_FAILURE и все связанные с ними проверки типов, потому что одна из функций в redux-saga - использовать этот богатый механизм "токенов" для создания эффектов и инструктирования Редукс магазин для легкого тестирования. Конечно, можно написать сагу, не используя эти действия, но это сделало бы ее похожей на Thunk.
С точки зрения структуры файлов, во многих случаях, как представляется, redux-saga более явный. Можно легко найти асинхронный код в каждом файле sagas.ts, но в избыточном коде его нужно будет увидеть в действиях.
Простое тестирование может быть еще одной взвешенной функцией в Redux-Saga. Это действительно удобно. Но одна вещь, которую необходимо пояснить, состоит в том, что тест "вызова" redux-saga не будет выполнять фактический вызов API в тестировании, поэтому необходимо будет указать пример результата для шагов, которые могут использовать его после вызова API. Поэтому, прежде чем писать в redux-saga, лучше спланировать сагу и соответствующие sagas.spec.ts подробно.
Redux-saga также предоставляет множество продвинутых функций, таких как параллельное выполнение задач, помощники по параллелизму, такие как takeLatest/takeEvery, fork/spawn, которые намного мощнее, чем thunks.
В заключение лично я хотел бы сказать: во многих нормальных случаях и приложениях от малого до среднего размера, используйте async/await style redux-thunk. Это избавит вас от множества стандартных кодов / действий /typedefs, и вам не нужно будет переключаться между многими различными sagas.ts и поддерживать определенное дерево саг. Но если вы разрабатываете большое приложение с очень сложной асинхронной логикой и нуждаетесь в таких функциях, как параллелизм / параллельный шаблон, или если у вас высокий спрос на тестирование и обслуживание (особенно в разработке, управляемой тестированием), возможно, что при этом вы сможете сэкономить вашу жизнь.,
В любом случае, redux-saga не более сложен и сложен, чем сам redux, и у него нет так называемой крутой кривой обучения, поскольку он имеет очень ограниченные основные концепции и API. Если вы потратите немного времени на изучение редукс-саги, это может принести вам пользу в будущем.
Изучив несколько различных крупномасштабных проектов React/Redux на своем опыте, Sagas предоставляет разработчикам более структурированный способ написания кода, который намного проще тестировать и труднее ошибиться.
Да, это немного странно для начала, но большинство разработчиков получают достаточно понимания за один день. Я всегда говорю людям не беспокоиться о том, что yield
делает для начала и что, как только вы напишите пару тестов, он придет к вам.
Я видел пару проектов, в которых thunk обрабатывался так, как если бы они были контроллерами из паттерна MVC, и это быстро превращается в необратимый беспорядок.
Мой совет - использовать Sagas там, где вам нужны триггеры типа B, относящиеся к одному событию. Я считаю, что для всего, что может затрагивать несколько действий, проще написать клиентское промежуточное ПО и использовать мета-свойство действия FSA для его запуска.
Thunks против саг
Redux-Thunk
а также Redux-Saga
отличаются по нескольким важным причинам, оба являются библиотеками промежуточного программного обеспечения для Redux (промежуточное программное обеспечение Redux - это код, который перехватывает действия, поступающие в хранилище с помощью метода dispatch()).
Действие может быть буквально любым, но если вы следуете передовым методам, это простой объект javascript с полем типа и необязательными полями полезной нагрузки, мета и ошибок. например
const loginRequest = {
type: 'LOGIN_REQUEST',
payload: {
name: 'admin',
password: '123',
}, };
Redux-Thunk
В дополнение к отправке стандартных действий, Redux-Thunk
промежуточное программное обеспечение позволяет отправлять специальные функции, называемые thunks
,
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'
отправляется в магазин. Как только это произойдет, выполнение будет продолжено.
Чтобы дать этому ответу некоторый контекст: Привет, я сопровождаю Redux.
Недавно мы добавили новую страницу Side Effects Approaches в документацию Redux, которая должна дать много информации обо всем этом, но я постараюсь написать здесь что-то короткое, так как этот вопрос широко освещается.
В 2022 году мы добавили промежуточное ПО Listener в официальный набор инструментов Redux для «реактивной логики Redux». Он может делать большинство вещей, которые могут саги (за исключением каналов), не требуя синтаксиса генератора и с лучшей поддержкой TypeScript.
Это не означает, что вы должны писать все с промежуточным программным обеспечением прослушивателя, однако мы рекомендуем всегда сначала использовать преобразователи, где это возможно, и использовать промежуточное программное обеспечение прослушивателя в дополнение, когда преобразователи не могут делать то, что вы хотите сделать.
Как правило, наша позиция на 2023 год заключается в том, что вы должны использовать саги только в том случае, если у вас есть особые потребности, которые не могут быть удовлетворены другим промежуточным программным обеспечением. (По существу: если вам нужны каналы.)
Наша рекомендация:
Получение данных
- Используйте RTK Query в качестве подхода по умолчанию для выборки и кэширования данных.
- Если RTKQ по какой-то причине не подходит полностью, используйте createAsyncThunk
- Возвращайтесь к рукописным преобразователям только в том случае, если больше ничего не работает
- Не используйте саги или наблюдаемые объекты для выборки данных!
Реагирование на действия/изменения состояния, асинхронные рабочие процессы
- Используйте прослушиватели RTK по умолчанию для ответа на обновления хранилища и написания длительных асинхронных рабочих процессов.
- Используйте саги / observables только в том случае, если слушатели недостаточно хорошо решают ваш вариант использования.
Логика с доступом к состоянию
- Используйте переходники для сложной синхронизации и умеренной асинхронной логики, включая доступ к getState и отправку нескольких действий.
Одна быстрая заметка. Генераторы отменяемы, асинхронные / ожидающие - нет. Так что для примера из вопроса, это действительно не имеет смысла, что выбрать. Но для более сложных потоков иногда нет лучшего решения, чем использование генераторов.
Итак, еще одна идея могла бы состоять в том, чтобы использовать генераторы с редуктором, но для меня это похоже на попытку изобрести велосипед с квадратными колесами.
И конечно, генераторы проще тестировать.
Недавно я присоединился к проекту, который интенсивно используетredux-saga
, поэтому мне также было интересно узнать больше о преимуществах подхода саги.
ТБХ, я все еще ищу. Прочитав этот пост и многим он понравился, «плюсы» неуловимы. Приведенные выше ответы, кажется, резюмируют это как:
- тестируемость (игнорирование реальных вызовов API),
- множество вспомогательных функций,
- знакомство для разработчиков, которые привыкли к кодированию на стороне сервера.
Многие другие утверждения кажутся оптимистичными, вводящими в заблуждение или просто ложными! Я видел много необоснованных утверждений, например, что «преобразователи не могут делать X». Но преобразователи — это функции. Если функция не может сделать X, то javascript не может сделать X. Так что саги тоже не могут сделать X.
Для меня МИНУСЫ:
- смешение опасений с помощью
generator
функции. Генераторы в JS возвращают пользовательские итераторы. Вот и все. У них нет особых возможностей для обработки асинхронных вызовов или отмены. Любой цикл может иметь условие прерывания, любая функция может обрабатывать асинхронные запросы, и любой код может использовать собственный итератор. Когда люди говорят что-то вроде:generators have control when to listen for some action
илиgenerators are cancellable, but async calls are not
затем это создает путаницу, подразумевая, что эти качества присущи генераторным функциям или даже уникальны для них. - неясные варианты использования: насколько я знаю, шаблон SAGA предназначен для обработки проблем с одновременными транзакциями в разных службах. Учитывая, что браузеры являются однопоточными, трудно понять, как параллелизм представляет собой проблему, которая
Promise
методы не могут справиться. Кстати: также трудно понять, почему этот класс проблем должен когда-либо обрабатываться в браузере. - прослеживаемость кода: с помощью ПО промежуточного слоя Redux для превращения
dispatch
в своего рода обработку событий Sagas отправляет действия, которые никогда не достигают редюсеров и поэтому никогда не регистрируются инструментами Redux. В то время как другие библиотеки также делают это, это часто излишне сложно, учитывая, что браузеры имеют встроенную обработку событий. Преимущество косвенности снова неуловимо, когда прямой вызов саги был бы более очевидным.
Если этот пост заставляет меня показаться разочарованным сагами, это потому, что я разочарован сагами. Они кажутся отличным решением, которое ищет проблему для решения. ИМО.
Вот проект, который сочетает в себе лучшие части (плюсы) обоих 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.
из документа
redux-auto исправил эту асинхронную проблему, просто позволив вам создать функцию "action", которая возвращает обещание. Для сопровождения вашей "стандартной" функции логика действий.
- Нет необходимости в другом промежуточном программном обеспечении Redux. например, Thunk, обещание промежуточного программного обеспечения, сага
- Позволяет легко передать обещание в редуксе и управлять им
- Позволяет совмещать внешние вызовы службы с тем, где они будут преобразованы
- Присвоение имени файлу init.js вызовет его один раз при запуске приложения. Это хорошо для загрузки данных с сервера при запуске
Идея состоит в том, чтобы каждое действие было в определенном файле. совместное размещение вызова сервера в файле с функциями редуктора для "ожидающих", "выполненных" и "отклоненных". Это делает обработку обещаний очень легкой.
Он также автоматически присоединяет вспомогательный объект (называемый "асинхронный") к прототипу вашего состояния, позволяя вам отслеживать в вашем пользовательском интерфейсе запрошенные переходы.