// actions.jsfunction showNotification(id, text) {return { type: 'SHOW_NOTIFICATION', id, text }}function hideNotification(id) {return { type: 'HIDE_NOTIFICATION', id }}
let nextNotificationId = 0export 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)}
import { createStore, applyMiddleware } from 'redux'import thunk from 'redux-thunk'
const store = createStore(reducer,applyMiddleware(thunk))
// It still recognizes plain object actionsstore.dispatch({ type: 'INCREMENT' })
// But with thunk middleware, it also recognizes functionsstore.dispatch(function (dispatch) {// ... which themselves may dispatch many timesdispatch({ type: 'INCREMENT' })dispatch({ type: 'INCREMENT' })dispatch({ type: 'INCREMENT' })
setTimeout(() => {// ... even asynchronously!dispatch({ type: 'DECREMENT' })}, 1000)})
let nextNotificationId = 0export 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)}}
function* toastSaga() {
// Some config constantsconst 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 reallylet pendingToasts = []; // A queue of toasts waiting to be displayedlet activeToasts = []; // Toasts currently displayed
// Trigger the display of a toast for 4 secondsfunction* 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 toastsyield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)yield call(delay,ToastDisplayTime); // Wait 4 secondsyield put(events.toastHidden(toast)); // Hide the toastactiveToasts = _.without(activeToasts,toast); // Remove from active toasts}
// Everytime we receive a toast display request, we put that request in the queuefunction* toastRequestsWatcher() {while ( true ) {// Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatchedconst 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 queueconst [firstToast,...remainingToasts] = pendingToasts;pendingToasts = remainingToasts;// Fork means we are creating a subprocess that will handle the display of a single toastyield fork(displayToast,firstToast);// Add little delay so that 2 concurrent toast requests aren't display at the same timeyield 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 notificationHide = createLogic({// the action type that will trigger this logictype: '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 laterprocess({ getState, action }, dispatch) {setTimeout(() => {dispatch({ type: 'NOTIFICATION_CLEAR' });}, 5000);}});
//timeoutMiddleware.jsconst timeoutMiddleware = store => next => action => {
//If your action doesn't have any timeout attribute, fallback to the default handlerif(!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)}
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'));
const rootEpic = (action$) => {// sets the incoming constant as a stream// of actions with type NEW_NOTIFICATIONconst 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 alertconst 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 streamreturn outgoing;}