Parallel AJAX with Promises

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The last missing piece of data on the cart page is the item color. I want to show a box with the actual color... but for that, we need the hex value. All we have right now, if we look at the Vue Dev tools, is the color IRI. Of course, with another AJAX call, we could use that to get the color data!

Cool! Let's go! We'll do exactly what we did to fetch the product data. First, add a new colors data set to null:

... lines 1 - 28
<script>
... lines 30 - 35
export default {
name: 'ShoppingCart',
... lines 38 - 42
data() {
return {
... line 45
colors: null,
};
},
... lines 49 - 78
};
</script>
... lines 81 - 91

Then, below in the watcher, we can fetch all the colors and then set them on that data. Do that with const colorsResponse = await and then call a function we saw earlier: fetchColors(). Hit tab to auto-complete that so PhpStorm adds the import way up on top:

... lines 1 - 28
<script>
import { fetchColors } from '@/services/colors-service';
... lines 31 - 35
export default {
name: 'ShoppingCart',
... lines 38 - 42
data() {
return {
... line 45
colors: null,
};
},
... lines 49 - 67
watch: {
async cart() {
... lines 70 - 72
const colorsResponse = await fetchColors();
... lines 74 - 76
},
},
};
</script>
... lines 81 - 91

Back in the watcher function, all we need is this.colors = colorsResponse.data['hydra:member']:

... lines 1 - 28
<script>
import { fetchColors } from '@/services/colors-service';
... lines 31 - 35
export default {
name: 'ShoppingCart',
... lines 38 - 42
data() {
return {
... line 45
colors: null,
};
},
... lines 49 - 67
watch: {
async cart() {
... lines 70 - 72
const colorsResponse = await fetchColors();
... lines 74 - 75
this.colors = colorsResponse.data['hydra:member'];
},
},
};
</script>
... lines 81 - 91

We could make this smarter - like we did with products - where we only fetch the color data that we need based on the items in the cart. But for our site, there are very few total colors, so I've decided to fetch all of them.

Now update completeCart(). First, if the colors data is not set yet - if not this.colors - then we should also return null from completeCart().

... lines 1 - 29
<script>
... lines 31 - 36
export default {
name: 'ShoppingCart',
... lines 39 - 49
computed: {
completeCart() {
if (!this.cart || !this.products || !this.colors) {
return null;
}
... lines 55 - 66
},
},
... lines 69 - 79
};
</script>
... lines 82 - 92

Inside the object, use the same trick we used to find the matching product: paste, and change the line to this.colors, color, color, and cartItem.color.

... lines 1 - 29
<script>
... lines 31 - 36
export default {
name: 'ShoppingCart',
... lines 39 - 49
computed: {
completeCart() {
if (!this.cart || !this.products || !this.colors) {
return null;
}
const completeItems = this.cart.items.map((cartItem) => (
{
product: this.products.find((product) => product['@id'] === cartItem.product),
color: this.colors.find((color) => color['@id'] === cartItem.color),
quantity: cartItem.quantity,
}
));
... lines 63 - 66
},
},
... lines 69 - 79
};
</script>
... lines 82 - 92

To see if this is working, up in the template, on a new line, print the hex color: if cartItem.color, then cartItem.color.hexColor - because hexColor is one of the fields we get back from the AJAX call. If the product does not have a color, print nothing.

<template>
<div :class="[$style.component, 'container-fluid']">
<div class="row">
... lines 4 - 5
<div class="col-xs-12 col-lg-9">
... lines 7 - 8
<div class="content p-3">
... line 10
<div v-if="completeCart !== null">
<div
... lines 13 - 14
>
... line 16
{{ cartItem.color ? cartItem.color.hexColor : '' }}
</div>
... lines 19 - 22
</div>
</div>
</div>
</div>
</div>
</template>
... lines 29 - 92

Testing time! Back at the browser... woohoo! My two inflatable sofas have a color. Those are going to look great in the office. Not all products have a color, but those that do, are now printing it.

Promise.all: Parallel Promises

But look back at the watcher function. I bet a lot of you spotted something wrong here: it's inefficient! We're making the first AJAX call, waiting and then starting the second AJAX call. Sometimes you do need to wait for one AJAX call to finish before you start another one... because the second one depends on the first. But that is not the case here: we should be able to start both of these at the same time so they run in parallel.

We could accomplish that by refactoring these into separate methods... or with the .then() syntax: using the response argument to set the products data. We could do the same for colors.

But... I'm going to undo that. As a challenge, let's pretend that both of these AJAX calls need to finish before we can run either of these lines of code. Like, maybe because we need to combine the data from both endpoints in some way.

So the question is: how can we start two AJAX calls at the same time, but then wait until both of them finish? The answer is with a cool Promise.all() function.

Check it out: say const and then an array with productResponse and colorResponse inside. Set that equal to await Promise.all(). Pass this the two promises that we need to wait for: fetchProductsbyId() and... after some reorganizing... fetchColors().

... lines 1 - 29
<script>
... lines 31 - 36
export default {
name: 'ShoppingCart',
... lines 39 - 68
watch: {
async cart() {
... lines 71 - 72
const [colorsResponse, productsResponse] = await Promise.all([
fetchColors(),
fetchProductsById(productIds),
]);
... lines 77 - 79
},
},
};
</script>
... lines 84 - 94

I love this! Promise.all() takes an array of Promises and returns a Promise. That Promise resolves once all of the internal promises resolve. The final resolved data is an array... and so we use array destructuring to set the colorResponse and productResponse variables. That's some fancy JavaScript!

And when we try it... hey! That fanciness even works!

Loading Data Event Earlier

But... as cool & hipster as this is, we don't really need it in this case. And we can make this even more performant. Think about it: because we're querying for all of the colors - and not just the colors for the items in the cart - we don't need to wait for the cart AJAX call to finish before fetching the colors. Nope, the color AJAX call can start immediately when the component is created.

At the bottom, add an async created(). Inside, say this.colors = await, then move the fetchColors() here... but also with the .data['hydra:member'] on it. Oh, and make sure this is called created not create.

... lines 1 - 29
<script>
... lines 31 - 36
export default {
name: 'ShoppingCart',
... lines 39 - 68
watch: {
async cart() {
const productIds = this.cart.items.map((item) => item.product);
const productsResponse = await fetchProductsById(productIds);
this.products = productsResponse.data['hydra:member'];
},
},
async created() {
this.colors = (await fetchColors()).data['hydra:member'];
},
};
</script>
... lines 82 - 92

Thanks to this, up here when the cart changes - so after the cart AJAX call finishes - we only need to make the one AJAX call for products. I'll simplify this again: const productResponse = await, then fetchProductsById(). Once that's finished, set the products data. Oh, and don't forget your equal sign!

... lines 1 - 29
<script>
... lines 31 - 36
export default {
name: 'ShoppingCart',
... lines 39 - 68
watch: {
async cart() {
const productIds = this.cart.items.map((item) => item.product);
const productsResponse = await fetchProductsById(productIds);
this.products = productsResponse.data['hydra:member'];
},
},
async created() {
this.colors = (await fetchColors()).data['hydra:member'];
},
};
</script>
... lines 82 - 92

Let's make sure I didn't bust anything. Do a full refresh and... got it! Behind the scenes, the cart and colors AJAX calls both start immediately. Then, when the cart API call finishes, the products AJAX call starts.

We can see this down in the browser network tools, filtered to the XHR calls. The cart call starts first, then colors - though, you can see on the waterfall on the right that they started at almost the same moment. Then, later, after the cart call finishes, here is the products AJAX call. Pretty cool.

Method Refactoring

Oh, but before we keep going, I want to make one tiny change. Add a methods key at the bottom of the component with one method inside: async loadProducts():

... lines 1 - 29
<script>
... lines 31 - 36
export default {
name: 'ShoppingCart',
... lines 39 - 76
methods: {
async loadProducts() {
... lines 79 - 82
},
},
};
</script>
... lines 87 - 97

Move the three product AJAX lines from the cart watcher into this. Then, call it from the watcher: this.loadProducts().

... lines 1 - 29
<script>
... lines 31 - 36
export default {
name: 'ShoppingCart',
... lines 39 - 68
watch: {
async cart() {
this.loadProducts();
},
},
... lines 74 - 76
methods: {
async loadProducts() {
const productIds = this.cart.items.map((item) => item.product);
const productsResponse = await fetchProductsById(productIds);
this.products = productsResponse.data['hydra:member'];
},
},
};
</script>
... lines 87 - 97

There's no specific reason for this change. For me, it's more readable to give these three lines of code, a name, like loadProducts. It's now easier to understand that, when the cart changes, we load the products. I do this a lot in PHP by creating private methods.

Next: we have all the data we need! So let's make this page shine.

Leave a comment!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@fortawesome/fontawesome-free": "^5.15.1", // 5.15.1
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.3
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.2.0
        "eslint-plugin-import": "^2.19.1", // 2.22.1
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.12
        "vue-loader": "^15.9.1", // 15.9.4
        "vue-template-compiler": "^2.6.11", // 2.6.12
        "webpack-notifier": "^1.6.0" // 1.8.0
    }
}