Не попадайтесь в ловушку, думая, что библиотека должна прописывать, как все делать . Если вы хотите что-то сделать с тайм-аутом в JavaScript, вам нужно использовать setTimeout
. Нет никаких причин, по которым действия Redux должны быть другими.
Redux делает предлагает альтернативные способы борьбы с асинхронными вещами, но вы должны использовать только те , когда вы понимаете , что вы повторяете слишком много коды. Если у вас нет этой проблемы, используйте то, что предлагает язык, и найдите самое простое решение.
Написание асинхронного кода Inline
Это, безусловно, самый простой способ. И здесь нет ничего конкретного для Redux.
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Аналогично, изнутри подключенного компонента:
this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Единственное отличие состоит в том, что в подключенном компоненте у вас обычно нет доступа к самому хранилищу, но вы получаете либо одного, dispatch()
либо конкретного создателя действия, вводимого в качестве реквизита. Однако это не имеет никакого значения для нас.
Если вам не нравится делать опечатки при отправке одних и тех же действий из разных компонентов, вы можете извлечь создателей действий вместо того, чтобы отправлять встроенные объекты действий:
// actions.js
export function showNotification(text) {
return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
return { type: 'HIDE_NOTIFICATION' }
}
// component.js
import { showNotification, hideNotification } from '../actions'
this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => {
this.props.dispatch(hideNotification())
}, 5000)
Или, если вы ранее связали их с connect()
:
this.props.showNotification('You just logged in.')
setTimeout(() => {
this.props.hideNotification()
}, 5000)
До сих пор мы не использовали промежуточное программное обеспечение или другие передовые концепции.
Извлечение Async Action Creator
Приведенный выше подход хорошо работает в простых случаях, но вы можете обнаружить, что у него есть несколько проблем:
- Это заставляет вас дублировать эту логику везде, где вы хотите показать уведомление.
- У уведомлений нет идентификаторов, поэтому у вас будет состояние гонки, если вы покажете два уведомления достаточно быстро. Когда первый тайм-аут заканчивается, он отправляет
HIDE_NOTIFICATION
, ошибочно скрывая второе уведомление раньше, чем после тайм-аута.
Чтобы решить эти проблемы, вам нужно извлечь функцию, которая централизует логику тайм-аута и отправляет эти два действия. Это может выглядеть так:
// actions.js
function showNotification(id, text) {
return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
return { type: 'HIDE_NOTIFICATION', id }
}
let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
// Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION
// for the notification that is not currently visible.
// Alternatively, we could store the timeout ID and call
// clearTimeout(), but we’d still want to do it in a single place.
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
Теперь компоненты могут использовать, showNotificationWithTimeout
не дублируя эту логику или не имея условий гонки с различными уведомлениями:
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Почему showNotificationWithTimeout()
принимает dispatch
в качестве первого аргумента? Потому что для этого нужно отправлять действия в магазин. Обычно компонент имеет доступ к нему, dispatch
но поскольку мы хотим, чтобы внешняя функция контролировала диспетчеризацию, нам нужно предоставить ему контроль над диспетчеризацией.
Если вы экспортировали одно хранилище из какого-то модуля, вы можете просто импортировать его и dispatch
вместо этого прямо в него:
// store.js
export default createStore(reducer)
// actions.js
import store from './store'
// ...
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
const id = nextNotificationId++
store.dispatch(showNotification(id, text))
setTimeout(() => {
store.dispatch(hideNotification(id))
}, 5000)
}
// component.js
showNotificationWithTimeout('You just logged in.')
// otherComponent.js
showNotificationWithTimeout('You just logged out.')
Это выглядит проще, но мы не рекомендуем такой подход . Основная причина, по которой нам это не нравится, заключается в том, что он заставляет магазин быть синглтоном . Это очень затрудняет реализацию рендеринга сервера. . На сервере вы хотите, чтобы каждый запрос имел свое собственное хранилище, чтобы разные пользователи получали разные предварительно загруженные данные.
Магазин Singleton также усложняет тестирование. Вы больше не можете издеваться над магазином при тестировании создателей действий, поскольку они ссылаются на конкретный реальный магазин, экспортированный из определенного модуля. Вы даже не можете сбросить его состояние снаружи.
Поэтому, хотя технически вы можете экспортировать одноэлементное хранилище из модуля, мы не рекомендуем его. Не делайте этого, если вы не уверены, что ваше приложение никогда не добавит рендеринг сервера.
Возвращаясь к предыдущей версии:
// actions.js
// ...
let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
Это решает проблемы с дублированием логики и спасает нас от условий гонки.
Thunk Middleware
Для простых приложений подход должен быть достаточным. Не беспокойтесь о промежуточном программном обеспечении, если вы довольны им.
В больших приложениях, однако, вы можете найти определенные неудобства вокруг него.
Например, кажется неудачным, что мы должны пройти мимо dispatch
. Это усложняет разделение контейнерных и презентационных компонентов, потому что любой компонент, который отправляет действия Redux асинхронно описанным выше способом, должен принять dispatch
в качестве подпорки, чтобы он мог пройти дальше. Вы больше не можете просто связывать создателей действий connect()
, потому что на showNotificationWithTimeout()
самом деле они не являются создателями действий. Он не возвращает действие Redux.
Кроме того, может быть неудобно вспоминать, какие функции похожи на создатели синхронных действий, showNotification()
а какие - на асинхронные помощники showNotificationWithTimeout()
. Вы должны использовать их по-разному и быть осторожным, чтобы не перепутать их друг с другом.
Это послужило мотивацией для поиска способа «узаконить» этот шаблон предоставления dispatch
вспомогательной функции и помочь Redux «увидеть» таких создателей асинхронных действий как особый случай создателей обычных действий, а не совершенно разные функции.
Если вы все еще с нами, и вы также признаете проблему в своем приложении, вы можете использовать промежуточное ПО Redux Thunk .
В сущности, Redux Thunk учит Redux распознавать особые виды действий, которые на самом деле являются функциями:
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(
reducer,
applyMiddleware(thunk)
)
// It still recognizes plain object actions
store.dispatch({ type: 'INCREMENT' })
// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) {
// ... which themselves may dispatch many times
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
dispatch({ type: 'INCREMENT' })
setTimeout(() => {
// ... even asynchronously!
dispatch({ type: 'DECREMENT' })
}, 1000)
})
Когда это промежуточное программное обеспечение включено, если вы отправляете функцию , промежуточное программное обеспечение Redux Thunk предоставит ее dispatch
в качестве аргумента. Он также «проглотит» такие действия, так что не беспокойтесь о том, что ваши редукторы получают странные аргументы функций. Ваши редукторы будут получать только простые действия с объектами - либо испускаемые напрямую, либо испускаемые функциями, как мы только что описали.
Это выглядит не очень полезно, не так ли? Не в этой конкретной ситуации. Однако это позволяет нам объявить showNotificationWithTimeout()
себя обычным создателем действий Redux:
// actions.js
function showNotification(id, text) {
return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
return { type: 'HIDE_NOTIFICATION', id }
}
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
Обратите внимание, что функция практически идентична той, которую мы написали в предыдущем разделе. Однако это не принимает dispatch
в качестве первого аргумента. Вместо этого он возвращает функцию, которая принимает dispatch
в качестве первого аргумента.
Как бы мы использовали его в нашем компоненте? Определенно, мы могли бы написать это:
// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)
Мы вызываем создателя асинхронного действия, чтобы получить внутреннюю функцию, которая хочет просто dispatch
, и затем мы переходим dispatch
.
Однако это даже более неловко, чем оригинальная версия! Почему мы даже пошли по этому пути?
Из-за того, что я говорил тебе раньше. Если промежуточное программное обеспечение Redux Thunk включено, то при каждой попытке отправки функции вместо объекта действия промежуточное программное обеспечение будет вызывать эту функцию с dispatch
самим методом в качестве первого аргумента .
Таким образом, мы можем сделать это вместо этого:
// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))
Наконец, отправка асинхронного действия (на самом деле, серии действий) выглядит не иначе, как синхронная отправка одного действия компоненту. Это хорошо, потому что компоненты не должны заботиться о том, происходит ли что-то синхронно или асинхронно. Мы просто абстрагировали это.
Обратите внимание, что, поскольку мы «научили» Redux распознавать таких «создателей специальных действий» (мы называем их создателями « большого действия»), мы можем теперь использовать их в любом месте, где мы будем использовать создателей обычных действий. Например, мы можем использовать их с connect()
:
// actions.js
function showNotification(id, text) {
return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
return { type: 'HIDE_NOTIFICATION', id }
}
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
// component.js
import { connect } from 'react-redux'
// ...
this.props.showNotificationWithTimeout('You just logged in.')
// ...
export default connect(
mapStateToProps,
{ showNotificationWithTimeout }
)(MyComponent)
Состояние чтения в Thunks
Обычно ваши редукторы содержат бизнес-логику для определения следующего состояния. Однако редукторы включаются только после отправки действий. Что если у вас есть побочный эффект (например, вызов API) в создателе thunk action, и вы хотите предотвратить его при определенных условиях?
Без использования промежуточного программного обеспечения Thunk, вы просто выполните эту проверку внутри компонента:
// component.js
if (this.props.areNotificationsEnabled) {
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}
Тем не менее, целью извлечения создателя действия была централизация этой повторяющейся логики во многих компонентах. К счастью, Redux Thunk предлагает вам прочитать текущее состояние магазина Redux. В дополнение к dispatch
этому он также передается getState
в качестве второго аргумента функции, которую вы возвращаете создателю thunk action. Это позволяет thunk прочитать текущее состояние магазина.
let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
return function (dispatch, getState) {
// Unlike in a regular action creator, we can exit early in a thunk
// Redux doesn’t care about its return value (or lack of it)
if (!getState().areNotificationsEnabled) {
return
}
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
}
Не злоупотребляйте этим паттерном. Это хорошо для спасения вызовов API, когда доступны кэшированные данные, но это не очень хорошая основа для построения вашей бизнес-логики. Если вы используетеgetState()
только условную диспетчеризацию различных действий, вместо этого поместить бизнес-логику в редукторы.
Следующие шаги
Теперь, когда у вас есть базовая интуиция о том, как работают thunks, посмотрите пример асинхронного Redux, который их использует.
Вы можете найти много примеров, в которых thunks возвращают обещания. Это не обязательно, но может быть очень удобно. Redux не заботится о том, что вы возвращаете из thunk, но дает вам возвращаемое значение dispatch()
. Вот почему вы можете вернуть Обещание из thunk и дождаться его завершения, позвонив dispatch(someThunkReturningPromise()).then(...)
.
Вы также можете разделить сложных создателей Thunk Action на несколько меньших создателей Thunk Action. dispatch
Метод , предоставляемый санков может принять санки самого по себе, так что вы можете применить шаблон рекурсивно. Опять же, это лучше всего работает с Promises, потому что вы можете реализовать асинхронный поток управления поверх этого.
Для некоторых приложений вы можете оказаться в ситуации, когда ваши требования к потоку асинхронного управления слишком сложны, чтобы их можно было выразить с помощью групповых символов. Например, повторная попытка неудачных запросов, поток повторной авторизации с токенами или пошаговая регистрация могут быть слишком многословными и подверженными ошибкам при написании таким образом. В этом случае вы можете захотеть взглянуть на более продвинутые решения асинхронного потока управления, такие как Redux Saga или Redux Loop . Оцените их, сравните примеры, соответствующие вашим потребностям, и выберите тот, который вам нравится больше всего.
Наконец, не используйте ничего (включая громоотводы), если у вас нет в них реальной необходимости. Помните, что в зависимости от требований ваше решение может выглядеть так же просто, как
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Не переживайте, если не знаете, почему вы это делаете.
redux-saga
основанный ответ, если вы хотите что-то лучше, чем гром. Поздний ответ, так что вам придется долго прокручивать его перед тем, как его увидеть :) Это не значит, что читать не стоит. Вот кратчайший путь: stackoverflow.com/a/38574266/82609