How to dispatch a Redux action with a timeout?

后端 未结 12 1819
花落未央
花落未央 2020-11-22 04:14

I have an action that updates the notification state of my application. Usually, this notification will be an error or info of some sort. I need to then dispatch another act

12条回答
  •  别那么骄傲
    2020-11-22 04:56

    Using Redux-saga

    As Dan Abramov said, if you want more advanced control over your async code, you might take a look at redux-saga.

    This answer is a simple example, if you want better explanations on why redux-saga can be useful for your application, check this other answer.

    The general idea is that Redux-saga offers an ES6 generators interpreter that permits you to easily write async code that looks like synchronous code (this is why you'll often find infinite while loops in Redux-saga). Somehow, Redux-saga is building its own language directly inside Javascript. Redux-saga can feel a bit difficult to learn at first, because you need basic understanding of generators, but also understand the language offered by Redux-saga.

    I'll try here to describe here the notification system I built on top of redux-saga. This example currently runs in production.

    Advanced notification system specification

    • You can request a notification to be displayed
    • You can request a notification to hide
    • A notification should not be displayed more than 4 seconds
    • Multiple notifications can be displayed at the same time
    • No more than 3 notifications can be displayed at the same time
    • If a notification is requested while there are already 3 displayed notifications, then queue/postpone it.

    Result

    Screenshot of my production app Stample.co

    Code

    Here I named the notification a toast but this is a naming detail.

    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)
        ]
    }
    

    And the reducer:

    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;
        }
    };
    

    Usage

    You can simply dispatch TOAST_DISPLAY_REQUESTED events. If you dispatch 4 requests, only 3 notifications will be displayed, and the 4th one will appear a bit later once the 1st notification disappears.

    Note that I don't specifically recommend dispatching TOAST_DISPLAY_REQUESTED from JSX. You'd rather add another saga that listens to your already-existing app events, and then dispatch the TOAST_DISPLAY_REQUESTED: your component that triggers the notification, does not have to be tightly coupled to the notification system.

    Conclusion

    My code is not perfect but runs in production with 0 bugs for months. Redux-saga and generators are a bit hard initially but once you understand them this kind of system is pretty easy to build.

    It's even quite easy to implement more complex rules, like:

    • when too many notifications are "queued", give less display-time for each notification so that the queue size can decrease faster.
    • detect window size changes, and change the maximum number of displayed notifications accordingly (for example, desktop=3, phone portrait = 2, phone landscape = 1)

    Honnestly, good luck implementing this kind of stuff properly with thunks.

    Note you can do exactly the same kind of thing with redux-observable which is very similar to redux-saga. It's almost the same and is a matter of taste between generators and RxJS.

提交回复
热议问题