Search code examples
javascriptreactjsreduxes6-promiseredux-thunk

How to call an asynchronous action creator in order?


Sourc code

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import { createStore, applyMiddleware, compose } from 'redux';
import { Provider } from 'react-redux';
import ReduxThunk from 'redux-thunk';
import reducer from './redux';

const body = document.querySelector('body'),
      composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose,
      store = createStore(reducer, composeEnhancers(applyMiddleware(ReduxThunk)));

ReactDOM.render(<Provider store={store}><App/></Provider>, body);

App.js

import React from 'react';
import Shortcut from './Shortcut';

export default class App extends React.PureComponent {
    render() {
        return <Shortcut/>;
    }
}

Shortcut.js

import React from 'react';
import { connect } from 'react-redux';
import { print_date_async } from './redux';

class Shortcut extends React.PureComponent {
    componentDidMount() {
        window.addEventListener('keydown', (event) => {
            if (event.keyCode === 13) {
                this.props.print_date_async({ date: new Date().toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1") });
            }
        });
    }

    render () {
        return null;
    }
}

function mapDispatchToProps(dispatch) {
    return {
        print_date_async: (date) => dispatch(print_date_async(date))
    };
}

Shortcut = connect(undefined, mapDispatchToProps)(Shortcut);

export default Shortcut;

redux.js

import { createAction, handleActions } from 'redux-actions';

export const print_date = createAction('print_date');

export function print_date_async (payload) {
    return async (dispatch) => {
        try {
            await wait_async();
            dispatch(print_date({ date:payload.date }));
        }
        catch (exeption) {
            console.error(exeption);
        }
    };
}

const initial_state = { };

export default handleActions({
    print_date: (state, action) => {
        console.log(action.payload.date);
        return { ...state }
    }
}, initial_state);

function wait_async (number) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, Math.floor(Math.random() * 10000)); // 0000 ~ 9999
    });
};

Problem

https://codesandbox.io/s/l7y4rn61k9

To explain the program I have created as an example, when you press enter, the time that you press enter is output after a random seconds later.

I want to have the next action creator called after one asynchronous action creator is called.

If you press and hold the Enter key, the result of the first press can also be printed later.

01:42:48
01:42:48
01:42:47
01:42:47
01:42:47
01:42:47
01:42:48

I considered exporting variables to check the status, but I did not like it. I also did not like checking the interval between pressing a key.

I want to do it in the following way, but it is not easy to implement. If you know about this, please answer. Thanks for reading!

window.addEventListener('keydown', (event) => {
    if (event.keyCode === 13) {
        if (!this.proceeding) {
            this.proceeding = true;
            (async () => {
                await this.props.print_date_async({ date: new Date().toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1") });
                this.proceeding = false;
            })();
        }
    }
});

Solve

I just need to return the promise object when using redux-thunk.

export function print_date_async (payload) {
    return (dispatch) => new Promise(async (resolve, reject) => {
        try {
            await wait_async();
            dispatch(print_date({ date:payload.date }));
            resolve();
        }
        catch (exeption) {
            console.error(exeption);
            reject(exeption);
        }
    });
}

I've know that async() returns promise object, so you don't have to wrap with promise.


Solution

  • The problem is that you have a stream of user input that creates a stream of asynchronous values. These values can resolve in a different order than the order of user input.

    For example: user clicks A,B and C this creates Promise A,B and C but they resolve in the order C,B and A. You would like them to resolve in the same order as the user actions right?

    Ordering or joining user generated streams with promises resolving is quite complex, maybe Reactive x can take care of this but the following code should do it:

    const resolveOrderedAndFast = (function(promiseInfos,requestIndex){
      const lastCall = {};
      return fastHandler => orderedHandler => promise => {
        requestIndex++;
        promise.then(fastHandler);
        promiseInfos.push([promise,orderedHandler,requestIndex]);
    
    
        //only resolve when it's last call
        const last = () => {
          lastCall.id={};
          var check = lastCall.id;
          return promise.then(
            resolve=>
              Promise.all(
                promiseInfos
                .sort((x,y)=>x[2]-y[2])
                .map(([p,fn,requestIndex])=>{
                  return p.then(
                    ([r,seconds])=>{
                      return [r,seconds,fn,requestIndex];
                    }
                  )
                })
              ).then(
                (resolves)=>{
                  if(check===lastCall.id){
                    resolves.forEach(
                      ([r,seconds,fn,requestIndex])=>
                        fn([r,seconds,requestIndex])
                    );
                    promiseInfos=[];
                    requestIndex=0;
                  }else{
                    //ignore, no problem
                  }
                }
              )
          );
        };
        last();//only resolve the last call to this function
      };
    }([],0))
    
    const later = resolveValue => {
      const values = ["A","B","C"];
      const index = values.indexOf(resolveValue);
      return new Promise(
        (resolve,reject)=>
          setTimeout(
            x=>resolve([resolveValue,(4-(index*2))])
            ,(4-(index*2))*1000
          )
      )
    };
    const fastHandler = val => ([resolve,seconds])=>
      console.log(
        val,
        "FAST HANDLER --- resolved with:",
        resolve,
        "in",seconds,"seconds"
      );
    const orderedHandler = val => ([resolve,seconds,i])=>
      console.log(
        "Call id:",
        i,
        "ORDRED HANDLER --- resolved with:",
        resolve,
        "in",seconds,"seconds"
      );
    const valueArray = ["A","B","C","A","B","C","A"];
    console.log("making request:",valueArray);
    valueArray
    .forEach(
      val=>
      resolveOrderedAndFast
        (fastHandler(val))
        (orderedHandler(val))
        (later(val))
    );
    
    setTimeout(
      ()=>
      console.log("making more requests:",valueArray) ||
      valueArray
        .forEach(
          val=>
          resolveOrderedAndFast
            (fastHandler(val))
            (orderedHandler(val))
            (later(val))
        )
      ,500
    );

    Here is a simpler version with some more comments on what it does:

    const Fail = function(reason){this.reason=reason;};
    const resolveOrderedAndFast = (function(preIndex,postIndex,stored){
      return fastHandler => orderedHandler => promise => {
        //call this function before the promise resolves
        const pre = p=> {
          preIndex++;
          p.then(fastHandler);
          (function(preIndex,handler){//set up post when promise resolved
            //add pre index, this will be 1,2,3,4
            //  but can resolve as 2,4,3,1
            p.then(r=>post([r,handler,preIndex]))
            //because the promises are stored here you may want to catch it
            //  you can then resolve with special Fail value and have orderedHandler
            //  deal with it
            .catch(e=>
              post(
                [
                  new Fail([e,r,preIndex]),//resolve to Fail value if rejected
                  handler,
                  preIndex
                ]
              )
            )
          })(preIndex,orderedHandler);//closure on index and ordered handler
        };
        //this will handle promise resolving
        const post = resolve=>{
          //got another resolved promise
          postIndex++;
          //store the details (it's resolve value, handler and index)
          stored.push(resolve);
          //deconstruct the resole value
          const [r,handler,i]=resolve;
          //the index of the resolve is same as current index
          //  that means we can start calling the resolvers
          if(i===postIndex){
            //sort the stored by their indexes (when they stared)
            stored = stored
            .sort((x,y)=>x[2]-y[2])
            //filter out all the ones that can be resolved
            .filter(
              ([r,handler,i])=>{
                //can be resolved, no promises have yet to be resolved
                //  before this index
                if(i<=postIndex){
                  //increase the number of indexes procssed (or stored)
                  postIndex++;
                  //resolve the value again (so we don't get errors)
                  Promise.resolve([r,i]).then(handler);
                  return false;//no need to "resolve" this again, filter it out
                }
                return true;
              }
            )
          }
        };
        pre(promise);
      };
    })(0,0,[])//passing in pre and post index and stored
    
    
    //demo on how to use it:
    const later = resolveValue => {
      const values = ["A","B","C"];
      const index = values.indexOf(resolveValue);
      return new Promise(
        (resolve,reject)=>
          setTimeout(
            x=>resolve([resolveValue,(4-(index*2))])
            ,(4-(index*2))*1000
          )
      )
    };
    const fastHandler = val => ([resolve,seconds])=>
      console.log(
        val,
        "FAST HANDLER --- resolved with:",
        resolve,
        "in",seconds,"seconds"
      );
    const orderedHandler = val => ([[resolve,seconds],i])=>
      console.log(
        "Call id:",
        i,
        "ORDRED HANDLER --- resolved with:",
        resolve,
        "in",seconds,"seconds"
      );
    const valueArray = ["A","B","C","A","B","C","A"];
    console.log("making request:",valueArray);
    valueArray
    .forEach(
      val=>
      resolveOrderedAndFast
        (fastHandler(val))
        (orderedHandler(val))
        (later(val))
    );
    
    setTimeout(
      ()=>
      console.log("making more requests:",valueArray) ||
      valueArray
        .forEach(
          val=>
          resolveOrderedAndFast
            (fastHandler(val))
            (orderedHandler(val))
            (later(val))
        )
      ,500
    );