Как отправить действие Redux с таймаутом?
У меня есть действие, которое обновляет состояние уведомления моего приложения. Обычно это уведомление будет ошибкой или какой-либо информацией. Затем мне нужно отправить еще одно действие через 5 секунд, которое вернет состояние уведомления в исходное состояние, поэтому никаких уведомлений нет. Основной причиной этого является обеспечение функциональности, при которой уведомления автоматически исчезают через 5 секунд.
Мне не повезло с использованием setTimeout
и возвращая другое действие и не могу найти, как это делается онлайн. Так что любые советы приветствуются.
15 ответов
Не попадайтесь в ловушку, думая, что библиотека должна прописывать, как все делать. Если вы хотите что-то сделать с тайм-аутом в 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 interval ID and call
// clearInterval(), 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.')
Это выглядит проще, но мы не рекомендуем такой подход. Главная причина, по которой нам это не нравится, заключается в том, что он заставляет магазин быть единичным. Это очень затрудняет реализацию рендеринга сервера. На сервере вы хотите, чтобы у каждого запроса было свое собственное хранилище, чтобы разные пользователи получали разные предварительно загруженные данные.
Синглтон-магазин также усложняет тестирование. Вы больше не можете издеваться над магазином при тестировании создателей действий, потому что они ссылаются на конкретный реальный магазин, экспортированный из определенного модуля. Вы даже не можете сбросить его состояние снаружи.
Поэтому, хотя технически вы можете экспортировать одноэлементное хранилище из модуля, мы не рекомендуем его. Не делайте этого, если вы не уверены, что ваше приложение никогда не добавит рендеринг сервера.
Возвращаясь к предыдущей версии:
// 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
метод, предоставляемый thunks, может принимать thunks сам, поэтому вы можете применять шаблон рекурсивно. Опять же, это лучше всего работает с Promises, потому что вы можете реализовать асинхронный поток управления поверх этого.
Для некоторых приложений вы можете оказаться в ситуации, когда ваши требования к потоку асинхронного управления слишком сложны, чтобы их можно было выразить с помощью блоков. Например, повторная попытка неудачных запросов, поток повторной авторизации с токенами или пошаговая регистрация могут быть слишком многословными и подверженными ошибкам при написании таким образом. В этом случае вы можете захотеть взглянуть на более продвинутые решения асинхронного потока управления, такие как Redux Saga или Redux Loop. Оцените их, сравните примеры, соответствующие вашим потребностям, и выберите тот, который вам нравится больше всего.
Наконец, не используйте ничего (включая громоотводы), если у вас нет в них реальной необходимости. Помните, что в зависимости от требований ваше решение может выглядеть так же просто, как
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
Не переживайте, если не знаете, почему вы это делаете.
Использование Redux-саги
Как сказал Дан Абрамов, если вы хотите более расширенный контроль над вашим асинхронным кодом, вы можете взглянуть на redux-saga.
Этот ответ является простым примером. Если вы хотите получить более подробные объяснения того, почему redux-saga может быть полезен для вашего приложения, отметьте этот другой ответ.
Основная идея заключается в том, что Redux-saga предлагает интерпретатор генераторов ES6, который позволяет вам легко писать асинхронный код, который выглядит как синхронный код (вот почему вы часто найдете бесконечные циклы while в Redux-saga). Каким-то образом Redux-saga создает свой собственный язык прямо внутри Javascript. Поначалу Redux-saga может показаться немного сложным в изучении, потому что вам нужно базовое понимание генераторов, но также и понимание языка, предлагаемого Redux-saga.
Я постараюсь здесь описать систему уведомлений, которую я построил на основе redux-saga. Этот пример в настоящее время работает в производстве.
Расширенная система уведомлений
- Вы можете запросить уведомление для отображения
- Вы можете запросить уведомление, чтобы скрыть
- Уведомление не должно отображаться более 4 секунд
- Несколько уведомлений могут отображаться одновременно
- Одновременно может отображаться не более 3 уведомлений.
- Если уведомление запрашивается, когда уже есть 3 отображаемых уведомления, то поставьте его в очередь / отложите.
Результат
Скриншот моего производственного приложения http://stample.co/
Код
Здесь я назвал уведомление toast
но это деталь именования.
function* toastSaga() {
// Some config constants
const MaxToasts = 3;
const ToastDisplayTime = 4000;
// Local generator state: you can put this state in Redux store
// if it's really important to you, in my case it's not really
let pendingToasts = []; // A queue of toasts waiting to be displayed
let activeToasts = []; // Toasts currently displayed
// Trigger the display of a toast for 4 seconds
function* displayToast(toast) {
if ( activeToasts.length >= MaxToasts ) {
throw new Error("can't display more than " + MaxToasts + " at the same time");
}
activeToasts = [...activeToasts,toast]; // Add to active toasts
yield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)
yield call(delay,ToastDisplayTime); // Wait 4 seconds
yield put(events.toastHidden(toast)); // Hide the toast
activeToasts = _.without(activeToasts,toast); // Remove from active toasts
}
// Everytime we receive a toast display request, we put that request in the queue
function* toastRequestsWatcher() {
while ( true ) {
// Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatched
const event = yield take(Names.TOAST_DISPLAY_REQUESTED);
const newToast = event.data.toastData;
pendingToasts = [...pendingToasts,newToast];
}
}
// We try to read the queued toasts periodically and display a toast if it's a good time to do so...
function* toastScheduler() {
while ( true ) {
const canDisplayToast = activeToasts.length < MaxToasts && pendingToasts.length > 0;
if ( canDisplayToast ) {
// We display the first pending toast of the queue
const [firstToast,...remainingToasts] = pendingToasts;
pendingToasts = remainingToasts;
// Fork means we are creating a subprocess that will handle the display of a single toast
yield fork(displayToast,firstToast);
// Add little delay so that 2 concurrent toast requests aren't display at the same time
yield call(delay,300);
}
else {
yield call(delay,50);
}
}
}
// This toast saga is a composition of 2 smaller "sub-sagas" (we could also have used fork/spawn effects here, the difference is quite subtile: it depends if you want toastSaga to block)
yield [
call(toastRequestsWatcher),
call(toastScheduler)
]
}
И редуктор:
const reducer = (state = [],event) => {
switch (event.name) {
case Names.TOAST_DISPLAYED:
return [...state,event.data.toastData];
case Names.TOAST_HIDDEN:
return _.without(state,event.data.toastData);
default:
return state;
}
};
использование
Вы можете просто отправить TOAST_DISPLAY_REQUESTED
События. Если вы отправите 4 запроса, отобразятся только 3 уведомления, а 4-й появится чуть позже, как только исчезнет 1-е уведомление.
Обратите внимание, что я не рекомендую отправлять TOAST_DISPLAY_REQUESTED
от JSX. Вы бы предпочли добавить еще одну сагу, которая прослушивает уже существующие события приложения, а затем отправить TOAST_DISPLAY_REQUESTED
Ваш компонент, который запускает уведомление, не должен быть тесно связан с системой уведомлений.
Заключение
Мой код не идеален, но работает с 0 ошибками в течение нескольких месяцев. Redux-saga и генераторы изначально немного сложны, но как только вы их поймете, такую систему довольно просто построить.
Даже довольно легко реализовать более сложные правила, такие как:
- когда слишком много уведомлений "ставятся в очередь", выделяйте меньше времени отображения для каждого уведомления, чтобы размер очереди мог уменьшаться быстрее.
- обнаруживать изменения размера окна и соответственно изменять максимальное количество отображаемых уведомлений (например, рабочий стол =3, портрет телефона = 2, ландшафт телефона = 1)
Честно говоря, удачи в правильной реализации такого рода вещей с помощью thunks.
Заметьте, что вы можете делать точно такие же вещи с redux-observable, что очень похоже на redux-saga. Это почти то же самое, и дело вкуса между генераторами и RxJS.
Репозиторий с примерами проектов
На данный момент существует четыре примера проектов:
- Написание асинхронного кода Inline
- Извлечение Async Action Creator
- Используйте Redux Thunk
- Используйте Redux Saga
Принятый ответ потрясающий.
Но чего-то не хватает:
- Нет запускаемых примеров проектов, только некоторые фрагменты кода.
- Нет примера кода для других альтернатив, таких как:
Поэтому я создал репозиторий Hello Async, чтобы добавить недостающие элементы:
- Работающие проекты. Вы можете скачать и запустить их без изменений.
- Предоставьте пример кода для большего количества альтернатив:
- Redux Saga
- Redux Loop
- ...
Redux Saga
Принятый ответ уже содержит примеры фрагментов кода для Async Code Inline, Async Action Generator и Redux Thunk. Для полноты картины приведу фрагменты кода для Redux Saga:
// actions.js
export const showNotification = (id, text) => {
return { type: 'SHOW_NOTIFICATION', id, text }
}
export const hideNotification = (id) => {
return { type: 'HIDE_NOTIFICATION', id }
}
export const showNotificationWithTimeout = (text) => {
return { type: 'SHOW_NOTIFICATION_WITH_TIMEOUT', text }
}
Действия просты и чисты.
// component.js
import { connect } from 'react-redux'
// ...
this.props.showNotificationWithTimeout('You just logged in.')
// ...
export default connect(
mapStateToProps,
{ showNotificationWithTimeout }
)(MyComponent)
Ничего особенного с компонентом.
// sagas.js
import { takeEvery, delay } from 'redux-saga'
import { put } from 'redux-saga/effects'
import { showNotification, hideNotification } from './actions'
// Worker saga
let nextNotificationId = 0
function* showNotificationWithTimeout (action) {
const id = nextNotificationId++
yield put(showNotification(id, action.text))
yield delay(5000)
yield put(hideNotification(id))
}
// Watcher saga, will invoke worker saga above upon action 'SHOW_NOTIFICATION_WITH_TIMEOUT'
function* notificationSaga () {
yield takeEvery('SHOW_NOTIFICATION_WITH_TIMEOUT', showNotificationWithTimeout)
}
export default notificationSaga
Саги основаны на генераторах ES6
// index.js
import createSagaMiddleware from 'redux-saga'
import saga from './sagas'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(saga)
По сравнению с Redux Thunk
Pros
- Вы не попадаете в ад обратного вызова.
- Вы можете легко проверить свои асинхронные потоки.
- Ваши действия остаются чистыми.
Cons
- Это зависит от генераторов ES6, который является относительно новым.
Пожалуйста, обратитесь к проекту runnable, если приведенные выше фрагменты кода не отвечают на все ваши вопросы.
Вы можете сделать это с помощью redux-thunk. В избыточном документе есть руководство по асинхронным действиям, таким как setTimeout.
Я бы порекомендовал также взглянуть на шаблон SAM.
Шаблон SAM требует включения "предиката следующего действия", при котором (автоматические) действия, такие как "уведомления исчезают автоматически через 5 секунд", запускаются после обновления модели (модель SAM ~ состояние редуктора + хранилище).
Шаблон поддерживает последовательное выполнение действий и мутаций модели по одному, поскольку "состояние управления" модели "контролирует", какие действия включены и / или автоматически выполняются предикатом следующего действия. Вы просто не можете предсказать (в общем), в каком состоянии будет система до обработки действия и, следовательно, будет ли разрешено / возможно ваше следующее ожидаемое действие.
Так, например, код,
export function showNotificationWithTimeout(dispatch, text) {
const id = nextNotificationId++
dispatch(showNotification(id, text))
setTimeout(() => {
dispatch(hideNotification(id))
}, 5000)
}
не будет разрешено с SAM, потому что тот факт, что действие hideNotification может быть отправлено, зависит от модели, успешно принимающей значение "showNotication: true". Могут существовать другие части модели, которые не позволяют ей принять его, и, следовательно, не будет причин для запуска действия hideNotification.
Я настоятельно рекомендую реализовать правильный предикат следующего действия после того, как магазин обновится, и новое состояние управления моделью станет известно. Это самый безопасный способ реализовать поведение, которое вы ищете.
Вы можете присоединиться к нам на Gitter, если хотите. Здесь также есть руководство по началу работы с SAM.
Попробовав различные популярные подходы (создатели действий, сценарии, саги, эпики, эффекты, пользовательское промежуточное программное обеспечение), я все же почувствовал, что, возможно, есть место для улучшений, поэтому я задокументировал свое путешествие в этой статье блога, куда я положил свою бизнес-логику приложение React/Redux?
Как и обсуждения здесь, я попытался сопоставить и сравнить различные подходы. Со временем это привело меня к представлению новой библиотеки лексемы-логики, которая черпает вдохновение из эпосов, саг, пользовательского промежуточного программного обеспечения.
Это позволяет вам перехватывать действия для проверки, подтверждения, авторизации, а также предоставлять способ выполнения асинхронного ввода-вывода.
Некоторые общие функции могут быть просто объявлены как отмена, регулирование, отмена и только использование ответа от последнего запроса (takeLatest). Избыточная логика оборачивает ваш код, предоставляя вам эту функциональность.
Это освобождает вас от реализации вашей основной бизнес-логики так, как вам нравится. Вам не нужно использовать наблюдаемые или генераторы, если вы не хотите. Используйте функции и обратные вызовы, обещания, асинхронные функции (async / await) и т. Д.
Код для простого уведомления 5s будет выглядеть примерно так:
const notificationHide = createLogic({
// the action type that will trigger this logic
type: 'NOTIFICATION_DISPLAY',
// your business logic can be applied in several
// execution hooks: validate, transform, process
// We are defining our code in the process hook below
// so it runs after the action hit reducers, hide 5s later
process({ getState, action }, dispatch) {
setTimeout(() => {
dispatch({ type: 'NOTIFICATION_CLEAR' });
}, 5000);
}
});
У меня есть более продвинутый пример уведомления в моем репо, который работает аналогично тому, что описал Себастьян Лорбер, где вы можете ограничить отображение до N элементов и поворачивать все, что находится в очереди. пример уведомления с избыточной логикой
У меня есть разнообразные живые примеры с избыточной логикой, а также полные примеры. Я продолжаю работать над документами и примерами.
Я хотел бы услышать ваши отзывы.
Я понимаю, что этот вопрос немного устарел, но я собираюсь представить другое решение, использующее избыточно наблюдаемый ака. Эпическая.
Цитирование официальной документации:
Что такое наблюдаемый редукс?
Промежуточное ПО на базе RxJS 5 для Redux. Составьте и отмените асинхронные действия, чтобы создать побочные эффекты и многое другое.
Эпос является основным примитивом наблюдаемого редукса.
Это функция, которая принимает поток действий и возвращает поток действий. Действия в, действия вне.
Другими словами, вы можете создать функцию, которая получает действия через поток и затем возвращает новый поток действий (используя общие побочные эффекты, такие как тайм-ауты, задержки, интервалы и запросы).
Позвольте мне опубликовать код, а затем объяснить немного больше об этом
store.js
import {createStore, applyMiddleware} from 'redux'
import {createEpicMiddleware} from 'redux-observable'
import {Observable} from 'rxjs'
const NEW_NOTIFICATION = 'NEW_NOTIFICATION'
const QUIT_NOTIFICATION = 'QUIT_NOTIFICATION'
const NOTIFICATION_TIMEOUT = 2000
const initialState = ''
const rootReducer = (state = initialState, action) => {
const {type, message} = action
console.log(type)
switch(type) {
case NEW_NOTIFICATION:
return message
break
case QUIT_NOTIFICATION:
return initialState
break
}
return state
}
const rootEpic = (action$) => {
const incoming = action$.ofType(NEW_NOTIFICATION)
const outgoing = incoming.switchMap((action) => {
return Observable.of(quitNotification())
.delay(NOTIFICATION_TIMEOUT)
//.takeUntil(action$.ofType(NEW_NOTIFICATION))
});
return outgoing;
}
export function newNotification(message) {
return ({type: NEW_NOTIFICATION, message})
}
export function quitNotification(message) {
return ({type: QUIT_NOTIFICATION, message});
}
export const configureStore = () => createStore(
rootReducer,
applyMiddleware(createEpicMiddleware(rootEpic))
)
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {configureStore} from './store.js'
import {Provider} from 'react-redux'
const store = configureStore()
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
App.js
import React, { Component } from 'react';
import {connect} from 'react-redux'
import {newNotification} from './store.js'
class App extends Component {
render() {
return (
<div className="App">
{this.props.notificationExistance ? (<p>{this.props.notificationMessage}</p>) : ''}
<button onClick={this.props.onNotificationRequest}>Click!</button>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
notificationExistance : state.length > 0,
notificationMessage : state
}
}
const mapDispatchToProps = (dispatch) => {
return {
onNotificationRequest: () => dispatch(newNotification(new Date().toDateString()))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
Код ключа для решения этой проблемы так же прост, как и круговая диаграмма, единственное, что отличается от других ответов, - это функция rootEpic.
Пункт 1. Как и в случае саг, вы должны объединить эпосы, чтобы получить функцию верхнего уровня, которая получает поток действий и возвращает поток действий, поэтому вы можете использовать ее с фабрикой промежуточного программного обеспечения createEpicMiddleware. В нашем случае нам нужен только один, поэтому у нас есть только rootEpic, поэтому нам не нужно ничего комбинировать, но это хорошо знать.
Пункт 2. Наш rootEpic, который заботится о логике побочных эффектов, занимает всего около 5 строк кода, и это здорово! Включая тот факт, что это в значительной степени декларативно!
Пункт 3. Построчно rootEpic объяснение (в комментариях)
const rootEpic = (action$) => {
// sets the incoming constant as a stream
// of actions with type NEW_NOTIFICATION
const incoming = action$.ofType(NEW_NOTIFICATION)
// Merges the "incoming" stream with the stream resulting for each call
// This functionality is similar to flatMap (or Promise.all in some way)
// It creates a new stream with the values of incoming and
// the resulting values of the stream generated by the function passed
// but it stops the merge when incoming gets a new value SO!,
// in result: no quitNotification action is set in the resulting stream
// in case there is a new alert
const outgoing = incoming.switchMap((action) => {
// creates of observable with the value passed
// (a stream with only one node)
return Observable.of(quitNotification())
// it waits before sending the nodes
// from the Observable.of(...) statement
.delay(NOTIFICATION_TIMEOUT)
});
// we return the resulting stream
return outgoing;
}
Я надеюсь, что это помогает!
Почему это должно быть так сложно? Это просто логика интерфейса. Используйте выделенное действие для установки данных уведомления:
dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })
и выделенный компонент для его отображения:
const Notifications = ({ notificationData }) => {
if(notificationData.expire > this.state.currentTime) {
return <div>{notificationData.message}</div>
} else return null;
}
В этом случае вопросы должны быть: "Как очистить старое состояние?", "Как уведомить компонент об изменении времени".
Вы можете реализовать некоторое действие TIMEOUT, которое отправляется на setTimeout от компонента.
Может быть, это просто, чтобы очистить его всякий раз, когда отображается новое уведомление.
Во всяком случае, должны быть некоторые setTimeout
где-то, верно? Почему бы не сделать это в компоненте
setTimeout(() => this.setState({ currentTime: +new Date()}),
this.props.notificationData.expire-(+new Date()) )
Мотивация заключается в том, что функциональность "исчезновения уведомлений" действительно является проблемой пользовательского интерфейса. Так что это упрощает тестирование вашей бизнес-логики.
Кажется, нет смысла проверять, как это реализовано. Имеет смысл проверить, когда время ожидания уведомления истекло. Таким образом, меньше кода на заглушку, быстрее тесты, более чистый код.
Если вам нужна обработка тайм-аута для отдельных действий, вы можете попробовать подход промежуточного программного обеспечения. Я столкнулся с аналогичной проблемой для выборочной обработки действий, основанных на обещаниях, и это решение было более гибким.
Допустим, ваш создатель действий выглядит так:
//action creator
buildAction = (actionData) => ({
...actionData,
timeout: 500
})
Тайм-аут может содержать несколько значений в вышеуказанном действии
- число в мс - в течение определенного времени ожидания
- true - для постоянной продолжительности тайм-аута. (обрабатывается в промежуточном программном обеспечении)
- undefined - для немедленной отправки
Ваша реализация промежуточного программного обеспечения будет выглядеть так:
//timeoutMiddleware.js
const timeoutMiddleware = store => next => action => {
//If your action doesn't have any timeout attribute, fallback to the default handler
if(!action.timeout) {
return next (action)
}
const defaultTimeoutDuration = 1000;
const timeoutDuration = Number.isInteger(action.timeout) ? action.timeout || defaultTimeoutDuration;
//timeout here is called based on the duration defined in the action.
setTimeout(() => {
next (action)
}, timeoutDuration)
}
Теперь вы можете направлять все свои действия через этот промежуточный слой, используя приставку.
createStore(reducer, applyMiddleware(timeoutMiddleware))
Вы можете найти несколько похожих примеров здесь
Надлежащий способ сделать это - использовать Redux Thunk, который является популярным промежуточным ПО для Redux, согласно документации Redux Thunk:
"Промежуточное программное обеспечение Redux Thunk позволяет писать создатели действий, которые возвращают функцию вместо действия. Thunk можно использовать для задержки отправки действия или для отправки только при соблюдении определенного условия. Внутренняя функция получает методы хранения диспетчеризация и getState в качестве параметров ".
Таким образом, в основном он возвращает функцию, и вы можете отложить отправку или перевести ее в состояние условия.
Так что-то вроде этого сделает работу за вас:
import ReduxThunk from 'redux-thunk';
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
function increment() {
return {
type: INCREMENT_COUNTER
};
}
function incrementAsync() {
return dispatch => {
setTimeout(() => {
// Yay! Can invoke sync or async actions with `dispatch`
dispatch(increment());
}, 5000);
};
}
Redux сам по себе является довольно многословной библиотекой, и для таких вещей вам придется использовать что-то вроде Redux-thunk, которое даст dispatch
функция, так что вы сможете отправить закрытие уведомления через несколько секунд.
Я создал библиотеку для решения таких вопросов, как многословие и сочетаемость, и ваш пример будет выглядеть следующим образом:
import { createTile, createSyncTile } from 'redux-tiles';
import { sleep } from 'delounce';
const notifications = createSyncTile({
type: ['ui', 'notifications'],
fn: ({ params }) => params.data,
// to have only one tile for all notifications
nesting: ({ type }) => [type],
});
const notificationsManager = createTile({
type: ['ui', 'notificationManager'],
fn: ({ params, dispatch, actions }) => {
dispatch(actions.ui.notifications({ type: params.type, data: params.data }));
await sleep(params.timeout || 5000);
dispatch(actions.ui.notifications({ type: params.type, data: null }));
return { closed: true };
},
nesting: ({ type }) => [type],
});
Поэтому мы составляем синхронизирующие действия для отображения уведомлений внутри асинхронного действия, которое может запросить некоторую информацию в фоновом режиме или позже проверить, было ли уведомление закрыто вручную.
Это просто. используйте пакет trim-redux и напишите так в componentDidMout или другом месте и уничтожьте его в componentWillUnmount.
componentDidMount(){
this.tm = setTimeout(function(){
setStore({ age: 20 });
}, 3000);
}
componentWillUnmount(){
clearTimeout(this.tm);
}
Действия Redux могут просто возвращать простой объект , а не функции, обратные вызовы или асинхронные процессы. Для отправки их через веб-API, такой как метод timeout(), вы должны использовать промежуточное ПО redux-thunk . Он был создан для обработки такого процесса.
- Первая конфигурация redux-thunk через документацию redux-thunk
- Во-вторых, измените своего создателя действия следующим образом:
const yourAction = millisecond => dispatch => {
setTimeout(() => {
dispatch({
type: 'YOUR_ACTIION_TYPE',
payload: yourWhatEverPayload
})
}, millisecond)
}
Это может быть немного не по теме, но я хочу поделиться этим здесь, потому что я просто хотел удалить оповещения из состояния после заданного тайм-аута, то есть автоматического скрытия предупреждений / уведомлений.
В итоге я использовал
setTimeout()
в пределах
<Alert />
компонент, чтобы затем он мог вызвать и отправить
REMOVE
действие по данному
id
.
export function Alert(props: Props) {
useEffect(() => {
const timeoutID = setTimeout(() => {
dispatchAction({
type: REMOVE,
payload: {
id: id,
},
});
}, timeout ?? 2000);
return () => clearTimeout(timeoutID);
}, []);
return <AlertComponent {...props} />;
}
Всякий раз, когда вы делаете setTimeout, пожалуйста, убедитесь, что вы также очищаете время ожидания, используя clearTimeout, когда компонент отключается в методе жизненного цикла componentWillUnMount.
store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
this.timeout = setTimeout(() => {
store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)
componentWillUnMount(){
clearTimeout(this.timeout);
}