Зачем нам промежуточное ПО для асинхронного потока в Redux?
Согласно документам: "Без промежуточного программного обеспечения хранилище Redux поддерживает только синхронный поток данных". Я не понимаю, почему это так. Почему компонент контейнера не может вызвать асинхронный API, а затем dispatch
действия?
Например, представьте простой пользовательский интерфейс: поле и кнопку. Когда пользователь нажимает кнопку, поле заполняется данными с удаленного сервера.
import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';
const ActionTypes = {
STARTED_UPDATING: 'STARTED_UPDATING',
UPDATED: 'UPDATED'
};
class AsyncApi {
static getFieldValue() {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(Math.floor(Math.random() * 100));
}, 1000);
});
return promise;
}
}
class App extends React.Component {
render() {
return (
<div>
<input value={this.props.field}/>
<button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
{this.props.isWaiting && <div>Waiting...</div>}
</div>
);
}
}
App.propTypes = {
dispatch: React.PropTypes.func,
field: React.PropTypes.any,
isWaiting: React.PropTypes.bool
};
const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
switch (action.type) {
case ActionTypes.STARTED_UPDATING:
return { ...state, isWaiting: true };
case ActionTypes.UPDATED:
return { ...state, isWaiting: false, field: action.payload };
default:
return state;
}
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
(state) => {
return { ...state };
},
(dispatch) => {
return {
update: () => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
};
})(App);
export default class extends React.Component {
render() {
return <Provider store={store}><ConnectedApp/></Provider>;
}
}
Когда экспортированный компонент отображается, я могу нажать кнопку, и входные данные корректно обновляются.
Обратите внимание update
функция в connect
вызов. Он отправляет действие, которое сообщает приложению об обновлении, а затем выполняет асинхронный вызов. После завершения вызова предоставленное значение отправляется как полезная нагрузка другого действия.
Что не так с этим подходом? Почему я хотел бы использовать Redux Thunk или Redux Promise, как следует из документации?
РЕДАКТИРОВАТЬ: Я искал в репозитории Redux подсказки, и обнаружил, что создатели действий должны были быть чистыми функциями в прошлом. Например, вот пользователь, пытающийся дать лучшее объяснение асинхронного потока данных:
Сам создатель действия все еще является чистой функцией, но возвращаемая им функция thunk не обязательна, и он может выполнять наши асинхронные вызовы.
Создатели действий больше не обязаны быть чистыми. Итак, промежуточное программное обеспечение Thunk/ обещание определенно требовалось в прошлом, но кажется, что это больше не так?
13 ответов
Что не так с этим подходом? Почему я хотел бы использовать Redux Thunk или Redux Promise, как следует из документации?
В этом подходе нет ничего плохого. Это просто неудобно в большом приложении, потому что у вас будут разные компоненты, выполняющие одни и те же действия, вы можете отменить некоторые действия или оставить некоторые локальные состояния, такие как автоматически увеличивающиеся идентификаторы, близкими к создателям действий и т. Д. с точки зрения обслуживания, чтобы выделить действия создателей в отдельные функции.
Вы можете прочитать мой ответ на "Как отправить действие Redux с тайм-аутом" для более подробного ознакомления.
Промежуточное программное обеспечение, такое как Redux Thunk или Redux Promise, просто дает вам "синтаксический сахар" для отправки thunks или обещаний, но вам не нужно его использовать.
Таким образом, без какого-либо промежуточного программного обеспечения ваш создатель действий может выглядеть
// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}
Но с Thunk Middleware вы можете написать это так:
// action creator
function loadData(userId) {
return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}
Так что нет большой разницы. Что мне нравится в последнем подходе, так это то, что компоненту не важно, что создатель действий является асинхронным. Это просто звонки dispatch
как правило, он также может использовать mapDispatchToProps
связать создателя действий с коротким синтаксисом и т. д. Компоненты не знают, как реализованы создатели действий, и вы можете переключаться между различными асинхронными подходами (Redux Thunk, Redux Promise, Redux Saga) без изменения компонентов. С другой стороны, при первом, явном подходе ваши компоненты точно знают , что конкретный вызов является асинхронным и требует dispatch
для передачи по какому-либо соглашению (например, в качестве параметра синхронизации).
Также подумайте, как изменится этот код. Скажем, мы хотим иметь вторую функцию загрузки данных и объединить их в одном создателе действий.
При первом подходе мы должны помнить о том, какого типа создателя действий мы называем:
// action creators
function loadSomeData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
function loadOtherData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
);
}
function loadAllData(dispatch, userId) {
return Promise.all(
loadSomeData(dispatch, userId), // pass dispatch first: it's async
loadOtherData(dispatch, userId) // pass dispatch first: it's async
);
}
// component
componentWillMount() {
loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}
С действием Redux Thunk создатели могут dispatch
результат других действий создателей и даже не думаю, являются ли они синхронными или асинхронными:
// action creators
function loadSomeData(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
function loadOtherData(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
);
}
function loadAllData(userId) {
return dispatch => Promise.all(
dispatch(loadSomeData(userId)), // just dispatch normally!
dispatch(loadOtherData(userId)) // just dispatch normally!
);
}
// component
componentWillMount() {
this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}
При таком подходе, если вы позже захотите, чтобы создатели ваших действий посмотрели на текущее состояние Redux, вы можете просто использовать второй getState
аргумент передается в thunks без изменения вызывающего кода вообще:
function loadSomeData(userId) {
// Thanks to Redux Thunk I can use getState() here without changing callers
return (dispatch, getState) => {
if (getState().data[userId].isLoaded) {
return Promise.resolve();
}
fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
}
Если вам нужно изменить его на синхронный, вы также можете сделать это без изменения какого-либо вызывающего кода:
// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
return {
type: 'LOAD_SOME_DATA_SUCCESS',
data: localStorage.getItem('my-data')
}
}
Таким образом, преимущество использования промежуточного программного обеспечения, такого как Redux Thunk или Redux Promise, состоит в том, что компоненты не знают о том, как реализованы создатели действий, и заботятся ли они о состоянии Redux, являются ли они синхронными или асинхронными, а также вызывают ли они других создателей действий., Недостатком является небольшая косвенность, но мы считаем, что это стоит того в реальных приложениях.
Наконец, Redux Thunk и друзья - это всего лишь один из возможных подходов к асинхронным запросам в приложениях Redux. Другим интересным подходом является Redux Saga, который позволяет вам определять долго работающих демонов ("саг"), которые выполняют действия по мере их поступления, а также преобразуют или выполняют запросы перед выводом действий. Это перемещает логику от создателей действий в саги. Вы можете проверить это, а затем выбрать то, что подходит вам больше всего.
Я искал подсказки в репозитории Redux и обнаружил, что в прошлом создатели действий должны были быть чистыми функциями.
Это неверно Документы сказали это, но документы были не правы.
Создатели действий никогда не должны были быть чистыми функциями.
Мы исправили документы, чтобы отразить это.
Вы не
Но... вы должны использовать Redx-сагу:)
Дан Абрамов прав redux-thunk
но я расскажу немного больше о саге о редуксе, который очень похож, но более мощный.
Повелительный указ декларативный
- DOM: jQuery является обязательным / React является декларативным
- Монады: IO является обязательным / Free является декларативным
- Redux эффекты:
redux-thunk
является обязательным /redux-saga
декларативный
Когда у вас в руках есть трек, например, монада ввода-вывода или обещание, вы не можете легко знать, что он будет делать после выполнения. Единственный способ проверить thunk - это выполнить его и высмеять диспетчера (или весь внешний мир, если он взаимодействует с большим количеством материала...).
Если вы используете макеты, то вы не занимаетесь функциональным программированием.
Сквозь призму побочных эффектов макеты - это признак того, что ваш код нечист, и, на взгляд функционального программиста, доказательство того, что что-то не так. Вместо того, чтобы загружать библиотеку, чтобы помочь нам проверить, не поврежден ли айсберг, мы должны плыть вокруг него. Один хардкорный TDD/Java парень как-то спросил меня, как ты делаешь насмешки в Clojure. Ответ, мы обычно не делаем. Обычно мы видим в этом знак того, что нам нужно реорганизовать наш код.
Саги (как они были реализованы в redux-saga
) являются декларативными и, подобно компонентам Free monad или React, их гораздо проще тестировать без всяких насмешек.
Смотрите также эту статью:
в современных FP мы не должны писать программы - мы должны писать описания программ, которые затем мы можем анализировать, трансформировать и интерпретировать по желанию.
(На самом деле Redux-сага похожа на гибрид: поток обязателен, но эффекты декларативны)
Путаница: действия / события / команды...
В мире внешнего интерфейса существует большая путаница в отношении того, как могут быть связаны некоторые базовые концепции, такие как CQRS / EventSourcing и Flux / Redux, главным образом потому, что в Flux мы используем термин "действие", который иногда может представлять как императивный код (LOAD_USER
) и события (USER_LOADED
). Я считаю, что, как и в случае с источником событий, вы должны только отправлять события.
Использование саг на практике
Представьте себе приложение со ссылкой на профиль пользователя. Идиоматический способ справиться с этим с обоими промежуточными программами:
redux-thunk
<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>
function loadUserProfile(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
);
}
redux-saga
<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>
function* loadUserProfileOnNameClick() {
yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}
function* fetchUser(action) {
try {
const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
yield put({ type: 'USER_PROFILE_LOADED', userProfile })
}
catch(err) {
yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
}
}
Эта сага переводится как:
каждый раз, когда по имени пользователя нажимают, выбирают профиль пользователя и затем отправляют событие с загруженным профилем.
Как видите, есть некоторые преимущества redux-saga
,
Использование takeLatest
позволяет выразить, что вы заинтересованы только в том, чтобы получить данные последнего кликаемого имени пользователя (справиться с проблемами параллелизма в случае, если пользователь очень быстро нажимает на большое количество имен пользователей). Такого рода вещи сложны с громилой. Вы могли бы использовать takeEvery
если ты не хочешь такого поведения.
Вы держите создателей действий в чистоте. Обратите внимание, что все еще полезно сохранять actionCreators (в сагах put
и компоненты dispatch
), так как это может помочь вам добавить проверку действия (assertions/flow/typescript) в будущем.
Ваш код становится намного более тестируемым, так как эффекты декларативны
Вам больше не нужно инициировать rpc-подобные вызовы, такие как actions.loadUser()
, Ваш пользовательский интерфейс просто должен отправить то, что произошло. Мы только запускаем события (всегда в прошедшем времени!), А не действия больше. Это означает, что вы можете создавать развязанные "утки" или " ограниченные контексты" и что сага может выступать в качестве связующего звена между этими модульными компонентами.
Это означает, что вашими представлениями легче управлять, потому что им больше не нужно содержать слой перевода между тем, что произошло, и тем, что должно произойти как результат.
Например, представьте бесконечный вид прокрутки. CONTAINER_SCROLLED
может привести к NEXT_PAGE_LOADED
Но действительно ли ответственность за прокручиваемый контейнер лежит на том, стоит ли нам загружать другую страницу? Затем он должен знать о более сложных вещах, таких как, была ли успешно загружена последняя страница или уже есть страница, которая пытается загрузить, или нет больше элементов для загрузки? Я так не думаю: для максимального повторного использования прокручиваемый контейнер должен просто описывать, что он был прокручен. Загрузка страницы является "бизнес-эффектом" этого свитка
Некоторые могут возразить, что генераторы могут скрывать состояние за пределами хранилища избыточных данных с помощью локальных переменных, но если вы начнете организовывать сложные вещи внутри thunks, запуская таймеры и т.д., у вас все равно будет та же проблема. И есть select
эффект, который теперь позволяет получить состояние из вашего магазина Redux.
Sagas может путешествовать во времени, а также обеспечивает сложную регистрацию потока и инструменты разработки, над которыми в настоящее время работают. Вот несколько простых асинхронных потоков, которые уже реализованы:
Развязка
Саги не только заменяют редуксовых громов. Они поступают из бэкэнда / распределенных систем / источников событий.
Это очень распространенное заблуждение, что саги как раз здесь, чтобы заменить ваши редуксы и улучшить тестируемость. На самом деле, это просто деталь реализации redux-саги. Использование декларативных эффектов лучше, чем thunk для тестируемости, но шаблон саги может быть реализован поверх императивного или декларативного кода.
Во-первых, сага - это часть программного обеспечения, которая позволяет координировать длительные транзакции (конечная согласованность) и транзакции в разных ограниченных контекстах (жаргон на основе доменного дизайна).
Чтобы упростить это для внешнего мира, представьте, что есть widget1 и widget2. Когда нажата какая-то кнопка на widget1, это должно повлиять на widget2. Вместо того, чтобы соединить 2 виджета вместе (т.е. widget1 отправляет действие, которое нацелено на widget2), widget1 отправляет только то, что была нажата его кнопка. Затем сага прослушивает нажатие этой кнопки, а затем обновляет widget2, отправляя новое событие, о котором известно widget2.
Это добавляет уровень косвенности, который не нужен для простых приложений, но упрощает масштабирование сложных приложений. Теперь вы можете публиковать widget1 и widget2 в разных репозиториях npm, чтобы им никогда не приходилось узнавать друг о друге, не предоставляя им общий глобальный реестр действий. 2 виджета теперь ограничены контекстами, которые могут жить отдельно. Они не нуждаются друг в друге, чтобы быть последовательными и могут быть повторно использованы в других приложениях. Сага является связующим звеном между двумя виджетами, которые значимым образом координируют их для вашего бизнеса.
Несколько хороших статей о том, как структурировать ваше приложение Redux, в котором вы можете использовать Redux-saga по причинам разделения:
- http://jaysoo.ca/2016/02/28/organizing-redux-application/
- http://marmelab.com/blog/2015/12/17/react-directory-structure.html
- https://github.com/slorber/scalable-frontend-with-elm-or-redux
Конкретный вариант использования: система уведомлений
Я хочу, чтобы мои компоненты могли запускать отображение уведомлений в приложении. Но я не хочу, чтобы мои компоненты были тесно связаны с системой уведомлений, которая имеет свои собственные бизнес-правила (макс. 3 уведомления, отображаемые одновременно, очередь уведомлений, время отображения 4 секунды и т. Д.).
Я не хочу, чтобы мои компоненты JSX решали, когда будет отображаться / скрываться уведомление. Я просто даю ему возможность запросить уведомление и оставить сложные правила внутри саги. Подобные вещи довольно сложно реализовать с помощью обещаний или обещаний.
Я описал здесь, как это можно сделать с помощью саги
Почему это называется сага?
Термин "сага" происходит из бэкэнд-мира. Я первоначально представил Yassine (автора Redux-saga) этому термину в длительной дискуссии.
Первоначально этот термин был введен в статье. Предполагалось, что шаблон saga должен использоваться для обработки возможной согласованности в распределенных транзакциях, но его использование было расширено до более широкого определения бэкэнд-разработчиками, так что теперь оно также охватывает "менеджер процессов". шаблон (каким-то образом оригинальный шаблон саги является специализированной формой менеджера процессов).
Сегодня термин "сага" сбивает с толку, поскольку он может описывать 2 разные вещи. Поскольку он используется в redux-saga, он описывает не способ обработки распределенных транзакций, а способ координации действий в вашем приложении. redux-saga
можно было бы также назвать redux-process-manager
,
Смотрите также:
- Интервью Яссин об истории Redux-саги
- Келла Байт: Сларифинг саги
- Microsoft CQRS Journey: Сага о сагах
- Средний ответ Ясин
альтернативы
Если вам не нравится идея использования генераторов, но вас интересует шаблон саги и его свойства развязки, вы также можете добиться того же с помощью redux-observable, которое использует имя epic
описать точно такой же шаблон, но с RxJS. Если вы уже знакомы с Rx, вы будете чувствовать себя как дома.
const loadUserProfileOnNameClickEpic = action$ =>
action$.ofType('USER_NAME_CLICKED')
.switchMap(action =>
Observable.ajax(`http://data.com/${action.payload.userId}`)
.map(userProfile => ({
type: 'USER_PROFILE_LOADED',
userProfile
}))
.catch(err => Observable.of({
type: 'USER_PROFILE_LOAD_FAILED',
err
}))
);
Некоторые полезные ресурсы редукса-саги
- Redux-сага против Redux-thunk с асинхронным ожиданием
- Управление процессами в Redux Saga
- От действий Создателей к Сагам
- Игра Snake реализована с помощью Redux-саги
2017 советует
- Не злоупотребляйте Redux-сагой только ради ее использования. Только тестируемые вызовы API не стоят того.
- Не удаляйте thunks из вашего проекта для самых простых случаев.
- Не стесняйтесь отправить Thunks в
yield put(someActionThunk)
если это имеет смысл.
Если вы боитесь использовать Redux-saga (или Redux-observable), но вам просто нужен шаблон развязки, проверьте https://github.com/slorber/redux-dispatch-subscribe: он позволяет прослушивать диспетчеризацию и запускать новые диспетчеры в приемнике.
const unsubscribe = store.addDispatchListener(action => {
if (action.type === 'ping') {
store.dispatch({ type: 'pong' });
}
});
Краткий ответ: мне кажется вполне разумным подход к проблеме асинхронности. С парой предостережений.
У меня была очень похожая точка зрения, когда я работал над новым проектом, который мы только начали на моей работе. Я был большим поклонником элегантной системы Vanilla Redux для обновления хранилища и рендеринга компонентов таким образом, чтобы не выходить за пределы дерева компонентов React. Мне показалось странным зацепиться за этот элегантный dispatch
механизм для обработки асинхронности.
Я закончил тем, что использовал очень похожий подход к тому, что у вас есть в библиотеке, которую я учел в нашем проекте, который мы назвали response-redux-controller.
Я закончил тем, что не выбрал именно тот подход, который у вас есть выше, по нескольким причинам:
- Как вы пишете, эти диспетчерские функции не имеют доступа к магазину. Вы можете в некоторой степени обойти это, передав своим компонентам пользовательского интерфейса всю информацию, необходимую для функции диспетчеризации. Но я бы сказал, что это без необходимости связывает эти компоненты пользовательского интерфейса с логикой диспетчеризации. И что еще более проблематично, у диспетчерской функции нет очевидного способа получить доступ к обновленному состоянию в асинхронных продолжениях.
- Диспетчерские функции имеют доступ к
dispatch
сам через лексическую сферу. Это ограничивает возможности рефакторинга, как толькоconnect
заявление выходит из-под контроля - и это выглядит довольно громоздким только с этимupdate
метод. Поэтому вам нужна система, позволяющая вам составлять эти функции диспетчера, если вы разбиваете их на отдельные модули.
Взять вместе, вы должны настроить систему, чтобы dispatch
и магазин, который будет добавлен в ваши диспетчерские функции, а также параметры события. Мне известны три разумных подхода к внедрению этой зависимости:
- redux-thunk делает это функциональным образом, передавая их в ваш thunks (делая их совсем не thunks, по определению купола). Я не работал с другим
dispatch
Подходы промежуточного программного обеспечения, но я предполагаю, что они в основном одинаковы. - Контроллер реагирует на это с сопрограммой. В качестве бонуса он также дает вам доступ к "селекторам", которые являются функциями, которые вы, возможно, передали в качестве первого аргумента
connect
вместо того, чтобы работать непосредственно с сырым, нормализованным магазином. - Вы также можете сделать это объектно-ориентированным способом, введя их в
this
контекст, через различные возможные механизмы.
Обновить
Мне приходит в голову, что часть этой загадки является ограничением реактивного редукса. Первый аргумент connect
получает снимок состояния, но не отправку. Второй аргумент получает отправку, но не состояние. Ни один из аргументов не получает блокировку, закрывающуюся по текущему состоянию, для возможности просмотра обновленного состояния во время продолжения / обратного вызова.
Цель Абрамова - и в идеале каждого - состоит в том, чтобы просто инкапсулировать сложность (и асинхронные вызовы) в месте, где это наиболее уместно.
Где лучшее место для этого в стандартном потоке данных Redux? Как насчет:
- Редукторы? Ни за что. Они должны быть чистыми функциями без побочных эффектов. Обновление магазина - это серьезное, сложное дело. Не оскверняй это.
- Тупой вид компонентов? Определенно нет. У них есть одна проблема: представление и взаимодействие с пользователем, и они должны быть максимально простыми.
- Компоненты контейнера? Возможно, но неоптимально. Это имеет смысл в том, что контейнер - это место, где мы инкапсулируем некоторую сложность, связанную с представлением, и взаимодействуем с магазином, но:
- Контейнеры должны быть более сложными, чем тупые компоненты, но это все еще одна обязанность: обеспечить привязки между представлением и состоянием / хранилищем. Ваша асинхронная логика - это отдельная проблема.
- Поместив его в контейнер, вы бы заблокировали свою асинхронную логику в одном контексте для одного представления / маршрута. Плохая идея. В идеале все это можно использовать повторно и полностью отделить.
- Какой другой сервисный модуль? Плохая идея: вам нужно внедрить доступ к магазину, что является кошмаром в отношении ремонтопригодности / тестируемости. Лучше пойти по пути Redux и получить доступ к магазину только с помощью предоставленных API / моделей.
- Действия и Middlewares, которые их интерпретируют? Почему бы и нет?! Для начала, это единственный важный вариант, который мы оставили.:-) Более логично, что система действий - это не связанная логика выполнения, которую вы можете использовать где угодно. Он имеет доступ к магазину и может отправлять больше действий. Он несет единственную ответственность за организацию потока управления и данных вокруг приложения, и большинство асинхронных операций вписывается в это.
- А как насчет создателей действий? Почему бы просто не выполнить асинхронность там, а не в самих действиях и в Middleware?
- Первое и самое важное, создатели не имеют доступа к магазину, как промежуточное ПО. Это означает, что вы не можете отправлять новые непредвиденные действия, не можете читать из магазина, чтобы составить асинхронный файл и т. Д.
- Поэтому сохраняйте сложность в сложном месте, а все остальное - просто. Создатели могут быть простыми, относительно чистыми функциями, которые легко тестировать.
- А как насчет создателей действий? Почему бы просто не выполнить асинхронность там, а не в самих действиях и в Middleware?
Чтобы ответить на вопрос, который задают в начале:
Почему компонент контейнера не может вызвать асинхронный API, а затем отправить действия?
Имейте в виду, что эти документы предназначены для Redux, а не для Redux плюс React. Хранилища Redux, подключенные к компонентам React, могут делать именно то, что вы говорите, но хранилище Plain Jane Redux без промежуточного программного обеспечения не принимает аргументы для dispatch
кроме простых старых объектов.
Без промежуточного программного обеспечения вы, конечно, можете обойтись
const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));
Но это аналогичный случай, когда асинхронность оборачивается вокруг Redux, а не обрабатывается Redux. Таким образом, промежуточное ПО допускает асинхронность, изменяя то, что может быть передано непосредственно dispatch
,
Тем не менее, дух вашего предложения, я думаю, действителен. Конечно, есть и другие способы обработки асинхронности в приложении Redux + React.
Одним из преимуществ использования промежуточного программного обеспечения является то, что вы можете продолжать использовать создателей действий как обычно, не беспокоясь о том, как они подключены. Например, используяredux-thunk
код, который вы написали, будет выглядеть так
function updateThing() {
return dispatch => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
}
const ConnectedApp = connect(
(state) => { ...state },
{ update: updateThing }
)(App);
который ничем не отличается от оригинала - он просто немного перемешан - иconnect
не знает чтоupdateThing
является (или должен быть) асинхронным.
Если вы также хотели поддерживать обещания, заметки, саги или сумасшедших создателей нестандартных действий и декларативных действий, тогда Redux может сделать это, просто изменив то, что вы передаете dispatch
(иначе, что вы возвращаете из действий создателей). Никаких гадостей с компонентами React (или connect
звонки) обязательно.
Хорошо, давайте начнем с того, как сначала работает промежуточное ПО, что вполне отвечает на вопрос, это исходный код функцииpplyMiddleWare в Redux:
function applyMiddleware() {
for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
middlewares[_key] = arguments[_key];
}
return function (createStore) {
return function (reducer, preloadedState, enhancer) {
var store = createStore(reducer, preloadedState, enhancer);
var _dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: function dispatch(action) {
return _dispatch(action);
}
};
chain = middlewares.map(function (middleware) {
return middleware(middlewareAPI);
});
_dispatch = compose.apply(undefined, chain)(store.dispatch);
return _extends({}, store, {
dispatch: _dispatch
});
};
};
}
Посмотрите на эту часть, посмотрите, как наша рассылка стала функцией.
...
getState: store.getState,
dispatch: function dispatch(action) {
return _dispatch(action);
}
- Обратите внимание, что каждому промежуточному программному обеспечению будет присвоен
dispatch
а такжеgetState
функционирует как именованные аргументы.
Хорошо, вот как Redux-thunk как одно из наиболее часто используемых промежуточных программ для Redux представляет себя:
Промежуточное ПО Redux Thunk позволяет создавать создателей действий, которые возвращают функцию вместо действия. Thunk может использоваться для задержки отправки действия или для отправки только при соблюдении определенного условия. Внутренняя функция получает методы хранения dispatch и getState в качестве параметров.
Итак, как вы видите, он будет возвращать функцию вместо действия, что означает, что вы можете ждать и вызывать ее в любое время, когда захотите, поскольку это функция...
Так что, черт возьми, такое? Вот как это представлено в Википедии:
В компьютерном программировании thunk - это подпрограмма, используемая для добавления дополнительных вычислений в другую подпрограмму. Thunks в основном используются для задержки вычислений до тех пор, пока они не потребуются, или для вставки операций в начале или конце другой подпрограммы. У них есть множество других приложений для генерации кода компилятора и в модульном программировании.
Термин возник как шутливая производная от "думать".
Thunk - это функция, которая упаковывает выражение, чтобы задержать его оценку.
//calculation of 1 + 2 is immediate
//x === 3
let x = 1 + 2;
//calculation of 1 + 2 is delayed
//foo can be called later to perform the calculation
//foo is a thunk!
let foo = () => 1 + 2;
Посмотрите, насколько проста концепция и как она может помочь вам управлять вашими асинхронными действиями...
Это то, что вы можете жить без этого, но помните, что в программировании всегда есть лучшие, аккуратные и правильные способы сделать что-то...
Существуют создатели синхронных действий, а затем - создатели асинхронных действий.
Создатель синхронного действия - это тот, который, когда мы вызываем его, немедленно возвращает объект Action со всеми соответствующими данными, прикрепленными к этому объекту, и готов к обработке нашими редукторами.
Создатели асинхронных действий - это те, в которых потребуется немного времени, прежде чем он будет готов в конечном итоге отправить действие.
По определению, каждый раз, когда у вас есть создатель действия, который отправляет сетевой запрос, он всегда будет квалифицирован как создатель асинхронного действия.
Если вы хотите, чтобы создатели асинхронных действий были внутри приложения Redux, вам нужно установить нечто, называемое промежуточным программным обеспечением, которое позволит вам иметь дело с этими создателями асинхронных действий.
Вы можете убедиться в этом в сообщении об ошибке, в котором говорится, что для асинхронных действий используется пользовательское промежуточное ПО
Так что же такое промежуточное программное обеспечение и зачем оно нам нужно для асинхронного потока в Redux?
В контексте промежуточного программного обеспечения с избыточностью, такого как redux-thunk, промежуточное программное обеспечение помогает нам иметь дело с создателями асинхронных действий, поскольку это то, что Redux не может обработать из коробки.
С промежуточным программным обеспечением, интегрированным в цикл Redux, мы по-прежнему обращаемся к создателям действий, которые будут возвращать действие, которое будет отправлено, но теперь, когда мы отправляем действие, а не отправляем его напрямую всем нашим редукторам, мы собираемся сказать, что действие будет отправлено через все различное промежуточное ПО внутри приложения.
Внутри одного приложения Redux у нас может быть столько промежуточного программного обеспечения, сколько мы хотим. По большей части, в проектах, над которыми мы работаем, у нас будет одно или два промежуточных ПО, подключенных к нашему магазину Redux.
Промежуточное программное обеспечение - это простая функция JavaScript, которая будет вызываться при каждом отправляемом нами действии. Внутри этой функции промежуточное ПО имеет возможность остановить отправку действия любому из редукторов, оно может изменить действие или просто поиграться с действием любым способом, который вы, например, могли бы создать, промежуточное ПО, которое регистрирует консоль каждое действие, которое вы отправляете, просто для вашего удовольствия.
Существует огромное количество промежуточного программного обеспечения с открытым исходным кодом, которое вы можете установить как зависимости в свой проект.
Вы не ограничены использованием только промежуточного программного обеспечения с открытым исходным кодом или установкой его в качестве зависимостей. Вы можете написать свое собственное промежуточное программное обеспечение и использовать его в своем магазине Redux.
Одно из наиболее популярных применений промежуточного программного обеспечения (и получение вашего ответа) предназначено для работы с создателями асинхронных действий, вероятно, наиболее популярным промежуточным программным обеспечением является redux-thunk, и оно помогает вам иметь дело с создателями асинхронных действий.
Существует много других типов промежуточного программного обеспечения, которые также помогают вам работать с создателями асинхронных действий.
Использовать Redux-saga - лучшее промежуточное ПО в реализации React-redux.
Пример: store.js
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware } from 'redux';
import allReducer from '../reducer/allReducer';
import rootSaga from '../saga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
allReducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga);
export default store;
А потом saga.js
import {takeLatest,delay} from 'redux-saga';
import {call, put, take, select} from 'redux-saga/effects';
import { push } from 'react-router-redux';
import data from './data.json';
export function* updateLesson(){
try{
yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js
}
catch(e){
console.log("error",e)
}
}
export function* updateDetail(action) {
try{
//To write store update details
}
catch(e){
console.log("error",e)
}
}
export default function* rootSaga(){
yield [
updateLesson()
]
}
А потом action.js
export default function updateFruit(props,fruit) {
return (
{
type:"UPDATE_DETAIL",
payload:fruit,
props:props
}
)
}
А потом Reducer.js
import {combineReducers} from 'redux';
const fetchInitialData = (state=[],action) => {
switch(action.type){
case "INITIAL_DATA":
return ({type:action.type, payload:action.payload});
break;
}
return state;
}
const updateDetailsData = (state=[],action) => {
switch(action.type){
case "INITIAL_DATA":
return ({type:action.type, payload:action.payload});
break;
}
return state;
}
const allReducers =combineReducers({
data:fetchInitialData,
updateDetailsData
})
export default allReducers;
А потом main.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/components/App.jsx';
import {Provider} from 'react-redux';
import store from './app/store';
import createRoutes from './app/routes';
const initialState = {};
const store = configureStore(initialState, browserHistory);
ReactDOM.render(
<Provider store={store}>
<App /> /*is your Component*/
</Provider>,
document.getElementById('app'));
попробуй это.. работает
Чтобы ответить на вопрос:
Почему компонент-контейнер не может вызвать асинхронный API, а затем отправить действия?
Я бы сказал как минимум по двум причинам:
Первая причина - разделение интересов, это не работа action creator
позвонить в api
и получить данные обратно, вам нужно передать два аргумента вашему action creator function
, то action type
и payload
.
Вторая причина в том, что redux store
ожидает простой объект с обязательным типом действия и необязательно payload
(но здесь вы также должны передать полезную нагрузку).
Создателем действия должен быть простой объект, как показано ниже:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
И работа Redux-Thunk midleware
к dispache
результат вашего api call
к соответствующему action
.
При работе в корпоративном проекте существует множество требований, доступных в промежуточном программном обеспечении, таких как (сага), недоступные в простом асинхронном потоке, некоторые из них:
- Выполнение запроса параллельно
- Вытягивание будущих действий без необходимости ждать
- Неблокирующие вызовы Эффект гонки, сначала пример подбора
- ответ на запуск процесса. Последовательность ваших задач (первый при первом вызове)
- Составление
- Отмена задачи. Динамическое разветвление задачи.
- Поддержка параллельного выполнения Saga вне промежуточного программного обеспечения redux.
- Использование каналов
Список длинный, просто просмотрите расширенный раздел в документации саги.
Я бы сказал по крайней мере по двум причинам:
Первая причина заключается в разделении задач, задача создателя действия не в том, чтобы вызывать API и возвращать данные, вам нужно передать два аргумента в функцию создателя действия, тип действия и полезную нагрузку.
Вторая причина заключается в том, что хранилище избыточности ожидает простой объект с обязательным типом действия и, возможно, полезную нагрузку (но здесь вы также должны передать полезную нагрузку).
Создатель действия должен быть простым объектом, как показано ниже:
function addTodo(text) {return {type: ADD_TODO,text}} И работа промежуточного программного обеспечения Redux-Thunk по отправке результата вашего API-вызова соответствующему действию.
Redux не может вернуть функцию вместо действия. Это просто факт. Вот почему люди используют Thunk. Прочтите эти 14 строк кода, чтобы увидеть, как он позволяет асинхронному циклу работать с некоторыми добавленными уровнями функций:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
Асинхронный означает, что если для выполнения задачи требуется время, она будет помещена в отдельное пространство памяти и не будет блокировать основной движок js, который является единственным стеком вызовов, который имеет движок js. Это поможет в выполнении кодов после этой задачи. Задача выполнится, как только стек вызовов станет пустым. Поэтому, если вы отправляете что-то со стороны клиента, в игру вступает промежуточное программное обеспечение. Самым популярным промежуточным программным обеспечением является thunk. Так мы рассматриваем каждую отправку, проходящую через нашу систему. Отправка — это всего лишь своего рода событие, содержащее название действия и данные, которые вы передаете со стороны клиента. Поэтому, если вы хотите запросить что-то из базы данных и использовать некоторые данные перед сохранением в Redux, тогда промежуточное программное обеспечение не будет выполняться, пока вы не получите ответ от базы данных, выполнив вызов API. Это просто приостановит исполнение. Таким образом, задача промежуточного программного обеспечения состоит в том, чтобы приостановить выполнение до тех пор, пока вы не получите ответ от базы данных.