Как отправить действие Redux с таймаутом?
У меня есть действие, которое обновляет состояние уведомления в моем приложении. Обычно это уведомление будет об ошибке или содержит некоторую информацию. После этого мне нужно отправить другое действие через 5 секунд, чтобы вернуть состояние уведомления к начальному, то есть убрать уведомление. Основная причина этого — предоставить функциональность, при которой уведомления автоматически исчезают через 5 секунд.
У меня не получилось использовать setTimeout
для возврата другого действия, и я не могу найти информацию о том, как это сделать онлайн. Буду благодарен за любые советы.
4 ответ(ов)
Я понимаю, что этот вопрос немного устарел, но я хотел бы предложить еще одно решение с использованием redux-observable, также известного как Epic.
Цитируя официальную документацию:
Что такое redux-observable?
Это промежуточное ПО для Redux, основанное на RxJS 5. Оно позволяет составлять и отменять асинхронные действия для создания побочных эффектов и многого другого.
Epic является основной примитивной единицей redux-observable.
Это функция, которая принимает поток действий и возвращает поток действий. Действия входящие, действия исходящие.
Проще говоря, вы можете создать функцию, которая принимает действия через поток, а затем возвращает новый поток действий (используя общие побочные эффекты, такие как таймауты, задержки, интервалы и запросы).
Позвольте мне поделиться кодом, а затем немного объяснить его.
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
case QUIT_NOTIFICATION:
return initialState
}
return state
}
const rootEpic = (action$) => {
const incoming = action$.ofType(NEW_NOTIFICATION)
const outgoing = incoming.switchMap((action) => {
return Observable.of(quitNotification())
.delay(NOTIFICATION_TIMEOUT)
});
return outgoing;
}
export function newNotification(message) {
return ({type: NEW_NOTIFICATION, message})
}
export function quitNotification() {
return ({type: QUIT_NOTIFICATION});
}
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. Как и с sagas, вам нужно комбинировать эпики, чтобы получить верхнеуровневую функцию, которая получает поток действий и возвращает поток действий, чтобы вы могли использовать ее с фабрикой промежуточного ПО createEpicMiddleware
. В нашем случае нам нужен только один, поэтому у нас есть только наш rootEpic
, и нам не нужно ничего комбинировать, но это полезный факт.
Пункт 2. Наш rootEpic
, который отвечает за логику побочных эффектов, занимает всего около 5 строк кода, что потрясающе! Тем более, что это довольно декларативно!
Пункт 3. Пошаговое объяснение rootEpic
(в комментариях):
const rootEpic = (action$) => {
// устанавливает incoming как поток
// действий с типом NEW_NOTIFICATION
const incoming = action$.ofType(NEW_NOTIFICATION)
// объединяет поток "incoming" с потоком,
// полученным для каждого вызова
// Эта функциональность аналогична flatMap (или Promise.all в некотором смысле)
// Она создает новый поток со значениями incoming и
// значениями, полученными из потока, сгенерированного функцией, переданной
// но останавливает объединение, когда incoming получает новое значение,
// в результате никакое действие quitNotification не будет установлено
// в результирующий поток
const outgoing = incoming.switchMap((action) => {
// создает observable со значением, переданным
// (поток с одним элементом)
return Observable.of(quitNotification())
// ждет перед отправкой узлов
// из выражения Observable.of(...)
.delay(NOTIFICATION_TIMEOUT)
});
// возвращаем результирующий поток
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()) )
Мотивация в том, что функциональность "затухания уведомления" действительно относится к пользовательскому интерфейсу. Это упрощает тестирование вашей бизнес-логики.
Не имеет смысла тестировать, как это реализовано. Имеет смысл проверить, когда уведомление должно исчезнуть. Таким образом, меньше кода для заглушек, более быстрые тесты, чистый код.
Подходящий способ решения этой задачи - использовать Redux Thunk, который является популярным промежуточным ПО для Redux. Согласно документации Redux Thunk:
"Промежуточное ПО Redux Thunk позволяет вам писать создателей действий, которые возвращают функцию вместо действия. Thunk может использоваться для задержки отправки действия или для отправки только в случае выполнения определенного условия. Внутренняя функция получает методы dispatch и getState хранилища в качестве параметров".
Таким образом, это означает, что вы можете вернуть функцию и задержать отправку вашего действия или установить условия для его отправки.
Пример кода, который выполнит вашу задачу:
import ReduxThunk from 'redux-thunk';
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
function increment() {
return {
type: INCREMENT_COUNTER
};
}
function incrementAsync() {
return dispatch => {
setTimeout(() => {
// Ура! Можете вызывать синхронные или асинхронные действия с помощью `dispatch`
dispatch(increment());
}, 5000);
};
}
Таким образом, используя Redux Thunk, вы можете легко управлять асинхронными действиями в вашем приложении.
Этот вопрос может быть немного не по теме, но я хочу поделиться решением, которое использовал для автоматического скрытия оповещений (Alerts) после определенного времени. Речь идет о том, как удалить Alerts из состояния по истечении тайм-аута.
Я решил использовать 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} />;
}
Этот подход позволяет автоматически скрывать оповещения через заданный период времени (по умолчанию 2000 миллисекунд, если не указано иное). Не забудьте очищать таймер при размонтировании компонента, чтобы избежать утечек памяти.
Где найти документацию по форматированию даты в JavaScript?
В чем разница между String.slice и String.substring?
Проверка соответствия строки регулярному выражению в JS
Существует ли ссылка на "последнюю" библиотеку jQuery в Google APIs?
Как создать диалог с кнопками "Ок" и "Отмена"