While working on my final Flatiron project, I had an issue where I needed the responses of several asynchronous tasks (fetches in my case) before moving forward with my code. I built an app that allows users to write their own pieces of interactive fiction. In the story editor component I set up the scene and path creation to create new scenes based on what the user enters in as paths. For example, a user could write a scene called “The Beginnning” that has three possible choices, “Enter the House,” “Search the Yard,” or “Run Away.” When the user is creating “The Beginning” these three scenes do not exist but the paths need their scene ids for backend persistence. I solved this issue using javascript’s Promise.all() method.

The Promise.all() method is best used in similar cases where the responses of multiple asynchronous tasks are required before other tasks can be completed. It takes an iterable, such as an Array, as an argument and, if the iterable contains promises, Promise.all() returns a pending promise that resolves to an array that contains the responses of all the promises in the iterable provided. This is best understood with an example, so I’ll walk you through my use case.

In my app, the user is creating stories with many scenes that are connected through paths. When a user starts a new story this new story is kept in state and has scenes added to it as they are created. Paths are an attribute of their scenes and stored as JSON data with their choice text and destination scene id.

Story.create(
  {
    title: "A Story about a House",
    user_id: 1,
    published: true,
    brief_description: "This is a house you don't want to call home."
  }
)

Scene.create(
  {
    title: "Beginning",
    story_id: 1,
    text: "You are standing in the street looking up at a house. Where should you start exploring?",
    paths: [
      {
        "scene_id": 2,
        "choice_text": "Explore the Yard"
      },
      {
        "scene_id": 3,
        "choice_text": "Walk through the Front Door"
      }
    ]
  }
)

Scene and path writing is set up as a form that updates the scene after the user fills out the form. Scenes are either created when a user clicks the “Add a Scene” button or when they make a path for a scene that doesn’t yet exist. When the user clicks submit, the program needs to do a few things. It will look at the paths the user created for the scene they wrote and search through the story in state to see if the destination scenes already exist. If they don’t the program will create them. Then, when all the destination scene ids are collected, the program will update the newly filled-out scene (the one the user is submitting) with those scene ids in the paths and render all the scenes for the story, including the empty scenes that were created when the user clicked submit.

In the image below I’ve started a story “Ladybug’s Adventure.” In the first scene, I’ve created three paths “Bathroom,” “Closet,” and “On Top of the Doors.” When I click submit, the steps I’ve outlined above will run.

Before Submit

When filling out the paths section of the form, the user can either select a scene from a drop down menu or type in a title to create a new scene. If the users selects an existing scene, I grab both the scene id and the choice text for the backend, but if the user types in a title, I have to create a new scene with that title before getting the scene id.

  onSubmit = (event) => {
    event.preventDefault()
    // take info from paths and create new scenes or find existing scenes
    let needNewScenePaths = []

    // create paths for sending to backend
    let paths = []

    // iterate through paths, if it's scene title need post request to get scene id, if pre-existing scene need scene_id

    this.state.paths.forEach(path => {
      if (path.scene_title){
        needNewScenePaths.push(path)
      } else {
        paths.push({
          "scene_id": path.selected_scene,
          "choice_text": path.choice_text
        })
      }
    })

    let promises = needNewScenePaths.map(path => 
      fetch("http://localhost:3001/scenes", {
        method: "POST",
        headers: {
          'content-type': 'application/json',
          'Authorization': `Bearer ${localStorage.token}`
        },
        body: JSON.stringify({
          story_id: `${this.props.story.id}`,
          title: `${path.scene_title}`,
          text: '',
          paths: []
        })
      })
      .then(res => res.json())
      .then(newScene => {
        paths.push({
          "scene_id": newScene.id,
          "choice_text": path.choice_text
        })
      })
    )

    Promise.all(promises).then(() => this.sceneUpdate(paths))
  }

In the code above, I map over the paths that need newly created scenes and the return value, an array in this case, is set to “promises.” Promise.all is passed “promises” and then it runs another function “sceneUpdate” after all the fetches have been completed and all the paths are ready to be updated in the filled-out scene.

SceneUpdate then takes the complete paths and updates the scene to reflect the user’s changes and it then calls the function getNewScenes. The final step in this process is updating the scenes in state and rendering the page with all the scenes of the story.

  sceneUpdate = (paths) => {
    fetch(`http://localhost:3001/scenes/${this.props.scene.id}`, {
      method: "PATCH",
      headers: {
        'content-type': 'application/json',
        'Authorization': `Bearer ${localStorage.token}`
      },
      body: JSON.stringify({
        story_id: `${this.props.story.id}`,
        title: `${this.state.title}`,
        text: `${this.state.text}`,
        paths: paths
      })
    })
    .then(res => res.json())
    .then(newScene => {
      // trigger fetch to render scenes
      this.props.getNewScenes()
    })
  }

  getNewScenes = () => {
    fetch(`http://localhost:3001/story_scenes/${this.state.story.id}`, {
      headers: {
        'Authorization': `Bearer ${localStorage.token}`
      },
    })
    .then(res => res.json())
    .then(scenes => {
      scenes.sort((a, b) => a.id - b.id)
      this.setState({
        scenes 
      })
    })
  }

In the image below, I’ve run this code with a few parts logged to the console. First, I logged the three scenes that needed to be created before “Where’s Ladybug?” can be updated. Then I logged the variable “promises” and all three scene promises were fulfilled. Then I logged paths after the three scenes were created and they now include their scene ids and all four scenes are rendered on the page.

After Submit

Javascript’s Promise.all() method was invaluable to me in this process. I wanted the user to be able to create scenes when writing paths and this would have been much more difficult without this method.

Source:

MDN Promise.all()