Как запускать AJAX-вызовы в ответ на изменения состояния с помощью Redux?
Я конвертирую существующую модель состояния в Redux, и по большей части она прошла безболезненно. Однако, одна проблема, с которой у меня возникают проблемы, - это преобразование "наблюдаемых" запросов Ajax-состояний. По сути, у меня есть определенные ajax-запросы, "связанные" с другими частями состояния, поэтому независимо от того, кто их изменяет, они всегда будут выполняться правильно. Я могу получить аналогичное поведение, подписавшись на обновления магазина Redux, но действия по запуску в слушателе кажутся хаком.
Возможное решение состоит в том, чтобы переместить логику создателю действия через шаблон thunk. Проблема в том, что мне придется либо дублировать выборочную логику между действиями (поскольку несколько действий могут изменить "наблюдаемое" состояние), либо перенести большую часть логики-восстановителя на уровень создателя действий. Создатель действия также не должен знать, как редукторы будут реагировать на выполненные действия.
Я мог бы пакетировать "подэтапы", поэтому мне нужно только разместить соответствующую логику выборки в каждом "блоке" действий, но это, кажется, нарушает концепцию действий, создающих допустимое состояние. Я бы предпочел эту ответственность на уровне создателей действий.
Существуют ли общепринятые правила, связанные с этим? Это не простое приложение, в котором специальные запросы AJAX выполняются при взаимодействии компонентов, большая часть данных распределяется между несколькими компонентами, а запросы оптимизируются и выбираются в ответ на изменение состояния.
TLDR; Я хочу запускать ajax-запросы в ответ на изменения состояния, а не когда происходит конкретное действие. Есть ли лучший, "специфичный для Redux" способ организации action/actionCreators, позволяющий имитировать это поведение, кроме запуска этих действий в подписчике подписчика?
4 ответа
С помощью store.subscribe()
Самый простой способ - просто использовать store.subscribe()
метод:
let prevState
store.subscribe(() => {
let state = store.getState()
if (state.something !== prevState.something) {
store.dispatch(something())
}
prevState = state
})
Вы можете написать собственную абстракцию, которая позволяет регистрировать условия для побочных эффектов, чтобы они были более декларативными.
Использование Redux Loop
Возможно, вы захотите взглянуть на Redux Loop, который позволяет вам описывать эффекты (такие как AJAX), вызовы вместе с обновлениями состояния в ваших редукторах.
Таким образом, вы можете "вернуть" эти эффекты в ответ на определенные действия, как вы в настоящее время return
следующее состояние:
export default function reducer(state, action) {
switch (action.type) {
case 'LOADING_START':
return loop(
{ ...state, loading: true },
Effects.promise(fetchDetails, action.payload.id)
);
case 'LOADING_SUCCESS':
return {
...state,
loading: false,
details: action.payload
};
Этот подход вдохновлен архитектурой вяза.
Использование Redux Saga
Вы также можете использовать Redux Saga, который позволяет вам писать долго выполняющиеся процессы ("саги"), которые могут выполнять действия, выполнять некоторую асинхронную работу и помещать действия с результатами в хранилище. Sagas следят за конкретными действиями, а не за обновлениями состояния, а это не то, о чем вы просили, но я решил, что на всякий случай упомяну их. Они отлично работают для сложных асинхронных потоков управления и параллелизма.
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED",message: e.message});
}
}
function* mySaga() {
yield* takeEvery("USER_FETCH_REQUESTED", fetchUser);
}
Нет единственно верного пути
Все эти варианты имеют разные компромиссы. Иногда люди используют один или два, или даже все три, в зависимости от того, что оказывается наиболее удобным для тестирования и описания необходимой логики. Я рекомендую вам попробовать все три и выбрать то, что лучше всего подходит для вашего варианта использования.
Вы можете использовать промежуточное ПО для запуска удаленных действий в ответ на локальные действия.
Допустим, у меня есть местное действие:
const updateField = (val) => {
{type: UPDATE_FIELD, val}
}
И поле ввода с:
<input type='text' onChange={this.props.updateField.bind(this.val)}>
Таким образом, в двух словах, когда вы печатаете внутри поля, оно запускает ваше действие, которое, в свою очередь, изменяет состояние через редуктор. Давайте просто забудем, как это действие было передано компоненту или что такое this.val - мы просто предполагаем, что это уже решено, и оно работает.
Все в порядке с этой настройкой, но она только меняет ваше состояние локально. Для обновления сервера вам придется запустить еще одно действие. Давайте построим это:
const updateFieldOnServer = (val) => {
return (dispatch) => {
MAKE_AJAX.done(
FIRE_SOME_ACTIONS_ON_SUCCESS
).failure(
FIRE_SOME_ACTIONS_ON_FAILURE
)
}
}
Это просто простое асинхронное действие thunk, которое каким-то образом выполняет ajax-запрос, возвращает обещания и делает что-то еще в случае успеха или неудачи.
Итак, проблема, с которой мы столкнулись сейчас, заключается в том, что я хочу, чтобы оба этих действия были запущены при изменении состояния ввода, но у меня не может быть onChange для выполнения двух функций. Поэтому я создам промежуточное ПО с именем ServerUpdatesMiddleware.
import _ from 'lodash'
import {
UPDATE_FIELD,
} from 'actionsPath'
export default ({ dispatch }) => next => action => {
if(_.includes([UPDATE_FIELD], action.type)){
switch(action.type){
case UPDATE_FIELD:
dispatch(updateFieldOnServer(action.val))
}
}
return next(action)
}
Я могу добавить его в свой стек:
import ServerUpdatesMiddleware from 'pathToMe'
const createStoreWithMiddleware = applyMiddleware(
ServerUpdatesMiddleware,
thunkMiddleware,
logger
)(createStore);
И прямо сейчас каждый раз, когда отправляется действие updateField, оно автоматически отправляет действие updateFieldOnServer.
Это всего лишь пример, который, я думаю, легко опишет проблему - эту проблему можно решить многими другими способами, но я думаю, что она хорошо соответствует требованиям. Это просто, как я делаю вещи - надеюсь, это поможет вам.
Я все время пользуюсь промежуточным программным обеспечением, и у меня их много - никогда не было проблем с этим подходом, и это упрощает логику приложения - вам нужно только посмотреть в одном месте, чтобы узнать, что происходит.
componentWillReceiveProps
Реакция жизненного цикла является лучшим местом для этого. componentWillReceiveProps
Будут переданы как новые, так и старые реквизиты, и внутри вы сможете проверить изменения и отправить свое действие, которое, в свою очередь, вызовет вызов ajax.
Но выгода здесь - это объект состояния, для которого вы проверяете, необходимо добавить его в качестве реквизита компонента через mapStateToProps
, так что он передается в componentWillReceiveProps. Надеюсь, поможет!
Мне кажется, что иметь модули, которые подписываются на обновления состояния и запускать Ajax-запросы (запускать действия по мере их появления), вполне нормально, так как это дает возможность хранилищам / редукторам твердо отвечать за инициирование запросов. В моем большом приложении все Ajax-запросы и другие асинхронные поведения выполняются таким образом, поэтому все действия могут быть просто полезными нагрузками, без понятия "создатели действий".
По возможности избегайте каскадных синхронизирующих действий. Мои асинхронные обработчики никогда не запускают действия синхронно, а только после завершения запроса.
На мой взгляд, это гораздо более функциональный подход, чем создатели асинхронных действий, которые вы можете или не можете предпочесть!