Search code examples
javascriptarraysreactjsfilteringsequential

React JS: Filtering an array of objects by another array of objects. How to sequential execute four functions incl. several API calls


The goal is to filter an array of objects by another array of objects. Each array comes from a different source.

The following setup might look weird, is though for several here unmentioned reasons unfortunately necessary.

  • Step one is to get from a Firebase database an array of objects with metadata about some (the latest) uploaded posts (allposts).
  • In step two this array will be filtered for userids and textids (filterArrayIds).
  • In step three for each filtered userid an API will be called to get detail informations to all existing posts of the corresponding userid. These will be merged into one array of posts (fetchJSONFiles).
  • In step four this merged array of posts should be filtered for all the textids in the array of textids of step two (filterJSON).

Below is my solution so far. Unfortunately I can't make it happen to execute the functions sequentially, so that especially fetchJSONfiles() is completely finished, before filterJSON() is called.

I am stuck here for hours ... any help is highly appreciated and would made my day. Thanks!

Example Data:

allposts: [
  {
    dateofpost: "1539181118111",
    textid: "1",
    userid: "Alice",
  },
  {
    dateofpost: "1539181118222",
    textid: "3",
    userid: "Bob",
  },
]

-

allfilteredTexts: [
  {
    title: "Lorem",
    textid: "1",
  },
  {
    title: "Ipsum",
    textid: "2",
  },
  {
    title: "Dolor",
    textid: "3",
  },
]

Expected Outcome:

latestPosts: [
  {
    title: "Lorem",
    textid: "1",
  },
  {
    title: "Dolor",
    textid: "3",
  },
]

My solution so far:

class Explore extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      allposts: [],
      textids: [],
      userids: [],
      allfilteredTexts: [],
    };
  }

  componentDidMount() {
    const allfilteredTexts = {...this.state.allfilteredTexts}
    firebase
      .firestore()
      .collection("allposts")
      .orderBy("dateofpost")
      .get()
      .then(snapshot => {
        const allposts = this.state.allposts;
        snapshot.forEach(doc => {
          allposts.push({
              userid: doc.data().userid,
              textid: doc.data().textid,
              dateofpost: doc.data().dateofpost,
          });
        });

        this.setState({
          allposts: allposts,
        });
      })
      .catch(function(error) {
        console.log("Error getting documents: ", error);
      })
      .then(() => {
              this.filterArrayIds();
      })
      .then(() => {
              this.fetchJSONFiles();
      })
      .finally(() => {
              this.filterJSON();
      });

    }


    filterArrayIds() {
      var userids = this.state.userids
      var textids = this.state.textids
      if (this.state.allposts) {
        var filtereduserids = [...new Set([].concat(...this.state.allposts.map(o => o.userid)))];
        var filteredtextids = [...new Set([].concat(...this.state.allposts.map(p => p.textid)))];
        this.setState({
            userids: filtereduserids,
            textids: filteredtextids,
        })
      }
    }

    fetchJSONFiles() {
      if (this.state.userids) {
         this.state.userids.forEach((username) => {
            var filteredTexts = []
            const options = {username} //here would be more API options //
            getFile(options)
              .then((file) => {
                filteredTexts = JSON.parse(file || '[]');
              })
              .then (() => {
                Array.prototype.push.apply(filteredTexts, this.state.allfilteredTexts);
                this.setState({
                  allfilteredTexts: filteredTexts,  
              })
          })
      }
    }

    filterJSON(){
          let latestPosts = (this.state.allfilteredTexts.filter(
            (el) => { return el.id.indexOf(this.state.textids) !== -1;
            }));
    }

    render () {

      return (
        <div>
              <Switch>
                <Route
                  path='/explore/latest/'
                  render={(props) => <ExploreLatest {...props} allposts={this.state.allposts} allfilteredTexts={this.state.allfilteredTexts} />}
                />
              </Switch>
        </div>
      )
    }
}
export default Explore;

Solution

  • I would suggest modifying like so:

    fetchJSONFiles() {
          if (this.state.userids) {
             return Promise.all(this.state.userids.map((username) => {
                var filteredTexts = []
                const options = {username} //here would be more API options //
                return getFile(options)
                  .then((file) => {
                    filteredTexts = JSON.parse(file || '[]');
                  })
                  .then (() => {
                    Array.prototype.push.apply(filteredTexts, this.state.allfilteredTexts);
                    this.setState({
                      allfilteredTexts: filteredTexts,  
                  })
              }))
          }
        }
    

    So then the lines:

     .then(() => {
              this.fetchJSONFiles();
      })
    

    Can become:

     .then(() => {
              return this.fetchJSONFiles();
      })
    

    Why?

    The reason why fetchJSONFiles doesn't finish before the rest of the Promise chain is because the Promise chain does not know to wait for the results of fetchJSONFiles. fetchJSONFiles makes asynchronous calls, therefore the rest of the synchronous code keeps on executing.

    However, by returning a Promise from fetchJSONFiles, we have something to "wait on". This uses the feature Promise.all, which basically says "create one promise that finishes when every promise in this array of promises finishes".

    Instead of forEach we use map, because this allows us to create a new array based on the base array, instead of just looping over it. And then instead of just calling getFile, we return the Promise chain. So we create an array of Promises from this.state.userids, and create one Promise from that which will resolve when all the fetches are done with Promise.all.

    Then we return that in the initial Promise chain which is located in componentDidMount. This tells the chain to wait for the result of that Promise (that Promise being the result of this.fetchJSONFiles) to be complete before continuing, which in this case would involve executing the finally callback.


    Now, there's certain other considerations to...consider. Namely, what happens if there's an error in one of the fetchJSONFiles call? That's something you'll have to think about, but these changes should just get you up and running to where you want to be.