Search code examples
javascriptpromiseecmascript-6es6-promisewaterfall

how to refactor waterfall .then()


This code here loads data using actions and would be series, but its going to be hard to edit this code add another API load, and the syntax is not clear.

this.props.loadNutMatrixes({perPage:'all'}).then(()=>{
      this.props.loadIngredients().then(()=>{
        this.props.getBadge().then(()=>{
          this.props.loadNutInfoItems({perPage:'all'}).then(()=>{
            this.props.getItemSize().then(()=>{
              this.props.getSingleMenuCategory(this.props.category_uid).then(()=>{
                this.props.loadAllStores(({per_page:'all'})).then(()=>{
                  if (this.props.selectedMenuItem ){
                    initialize("addNewMenuItem", {
                      ...this.props.selectedMenuItem
                    })
                  }
                })
              })
            })
          })
        })
      })
    })


Solution

  • You can convert this to a vertical structure by chaining the promises instead of nesting:

    this.props.loadNutMatrixes({perPage:'all'})
      .then(() => this.props.loadIngredients())
      .then(() => this.props.getBadge())
      .then(() => this.props.loadNutInfoItems({perPage:'all'}))
      .then(() => this.props.getItemSize())
      .then(() => this.props.getSingleMenuCategory(this.props.category_uid))
      .then(() => this.props.loadAllStores(({per_page:'all'})))
      .then(() => {
        if (this.props.selectedMenuItem) {
          initialize("addNewMenuItem", {
            ...this.props.selectedMenuItem
          })
        }
      });
    

    A possible improvement could be to wrap all promise-creating functions that receive arguments into functions without arguments and pass those as props instead:

    loadAllNutMatrixes() {
      return this.loadNutMatrixes({ perPage: 'all' });
    }
    
    loadAllNutInfoItems() {
      return this.loadNutInfoItems({ perPage: 'all' });
    }
    
    getSingleMenuCategoryFromId() {
      return this.getSingleMenuCategory(this.category_uid);
    }
    
    loadEveryStory() {
      return this.loadAllStores({ perPage: 'all' });
    }
    

    Then you could refactor the last step into its own method:

    onChainFinished() {
      if (this.props.selectedMenuItem) {
        initialize("addNewMenuItem", {
          ...this.props.selectedMenuItem
        })
      }
    }
    

    And combine the two with some destructuring to achieve a cleaner chain:

    const { props } = this;
    props.loadAllNutMatrixes()
      .then(props.loadIngredients)
      .then(props.getBadge)
      .then(props.loadAllNutInfoItems)
      .then(props.getItemSize)
      .then(props.getSingleMenuCategoryFromId)
      .then(props.loadEveryStore)
      .then(this.onChainFinished);
    

    EDIT based on your comment

    using something like promise.all but in series way !

    There is no native method to chain Promises but you can build a helper method suitable for your use case to do this. Here's a general example:

    // `cp` is a function that creates a promise and 
    // `args` is an array of arguments to pass into `cp`
    chainPromises([
      { cp: this.props.loadNutMatrixes, args: [{perPage:'all'}] },
      { cp: this.props.loadIngredients },
      { cp: this.props.getBadge },
      { cp: this.props.loadNutInfoItems, args: [{perPage:'all'}] },
      { cp: this.props.getItemSize },
      { cp: this.props.getSingleMenuCategory, args: [this.props.category_uid] },
      { cp: this.props.loadAllStores, args: [{per_page:'all'}] }
    ]).then(() => {
      if (this.props.selectedMenuItem) {
        initialize("addNewMenuItem", {
          ...this.props.selectedMenuItem
        })
      }
    });
    
    function chainPromises(promises) {
      return promises.reduce(
        (chain, { cp, args = [] }) => {
          // append the promise creating function to the chain
          return chain.then(() => cp(...args));
        }, Promise.resolve() // start the promise chain from a resolved promise
      );
    }
    

    If you use the same approach as above to refactor methods with arguments, it would clean this code up as well:

    const { props } = this;
    chainPropsPromises([
      props.loadAllNutMatrixes,
      props.loadIngredients,
      props.getBadge,
      props.loadAllNutInfoItems,
      props.getItemSize,
      props.getSingleMenuCategoryFromId,
      props.loadEveryStory
    ])
    .then(this.onChainFinished);
    
    function chainPropsPromises(promises) {
      return promises.reduce(
        (chain, propsFunc) => (
          chain.then(() => propsFunc());
        ), Promise.resolve()
      );
    }