Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Smarter Loading: Ajax status as State

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

Oh no! The snacks category is empty! That's a huge problem on its own! To make things worse, you can't even tell! It looks like it's loading forever... while I'm sitting here getting hungrier and hungrier.

The reason is that, in the product-list component, we're showing the loading animation by checking if the products length is zero.

Using the length of an array to figure out if something is done loading doesn't really work if it's possible that the thing really can be empty in some cases! And that's totally the situation we have: sometimes a category has no products. And later when we add a search, sometimes no products will match.

So... the easy solution didn't work. What we need instead is a flag that specifically tracks whether or not the products Ajax call is finished.

Add loading to Catalog

We know Catalog is the smart component that takes care of making the Ajax request. This is means that it is also aware of whether or not we are currently making an Ajax call for the products. To track this, let's add a new data: call it loading and set it to false by default.

... lines 1 - 18
<script>
... lines 20 - 23
export default {
... lines 25 - 35
data() {
return {
... line 38
loading: false,
... line 40
};
},
... lines 43 - 54
};
</script>

Now, very simply, in created(), say this.loading = true right before the Ajax call and, right after, this.loading = false.

And just like that, we have a flag that we can use to render things based on the true loading status!

... lines 1 - 42
async created() {
... lines 44 - 48
this.loading = true;
const response = await axios.get('/api/products', {
params,
});
this.loading = false;
... line 56
},
... lines 58 - 60

Try...catch

While we're here, we can also add some simple error handling just in case the Ajax call fails. To do that, wrap all of this in a try...catch block. Then, inside catch, set this.loading = false.

... lines 1 - 42
async created() {
... lines 44 - 50
try {
... lines 52 - 57
} catch (e) {
this.loading = false;
}
},
... lines 62 - 64

If we really didn't trust our API... we could add a data called error, change that in catch to a message and render it. But with this, we will at least fail somewhat gracefully, and avoid the loader from spinning forever.

As easy as that was, this could be a bit dangerous! The problem right now is that if any of these lines have an error - like if our response doesn't have a data key on it - then the catch will be called and we will not show it. We could be hiding a bug in our code. I hate bugs! I think pizza is much better...

So instead, above the try, add let response. This simply declares the variable outside of the try...catch scope so that it's available in the entire created function. Now, remove the const from response and then I'll return from the catch. So if we hit the catch, just exit. Finally, move the this.products = response.data code outside of the catch. Now if that line has a problem, it won't be silenced: we'll have to deal with it!

... lines 1 - 42
async created() {
... lines 44 - 48
this.loading = true;
let response;
try {
response = await axios.get('/api/products', {
params,
});
this.loading = false;
} catch (e) {
this.loading = false;
return;
}
this.products = response.data['hydra:member'];
},
... lines 66 - 68

Whether or not you should use the try...catch just depends on your situation. I probably wouldn't do this because, if my API endpoint is failing, I have bigger problems: my site is broken! Giving the user a graceful error is nice, but maybe I'll save that for V2.

However, if you do have a valid situation where an Ajax request might fail - like if you're sending data to the server that might fail validation - then this is how you can catch that error and deal with it. We'll talk about sending data in the next tutorial.

Pass loading down to product-list

Okay, we now have the loading data on our smart catalog component. Let's pass that into the product-list component so that we can use it to hide or show the loading spinner. Split the product-list onto multiple lines and then add :loading="loading".

<template>
<div>
... lines 3 - 10
<product-list
:products="products"
:loading="loading"
/>
... lines 15 - 18
</div>
</template>
... lines 21 - 71

And now that we're passing the loading prop, in index.vue, update the props so we can receive it: add a new loading prop with type: Boolean and required: true.

... lines 1 - 17
<script>
... lines 19 - 21
export default {
... lines 23 - 27
props: {
loading: {
type: Boolean,
required: true,
},
... lines 33 - 36
},
};
</script>

We can now simplify the template: we want to show the loading animation if loading is true. And we also want to show these product cards down here, if we are !loading. This second spot isn't super important, but it doesn't hurt to have it!

<template>
<div class="row">
<div class="col-12">
<div class="mt-4">
<loading v-show="loading" />
</div>
</div>
<product-card
... line 10
v-show="!loading"
... lines 12 - 13
/>
</div>
</template>
... lines 17 - 40

Adding a "No Products" Message after Loading

Time to check things out! Yep! You can already see that the snacks page no longer has the loading spinner. And my other pages work just fine.

Well... except it would be even better with a "no products found" message! And now, we can easily add that.

After the <loading /> component, add an h5 with a v-show directive. This will hold that "no products found" message... which means that we want it to show if we are not loading but products.length === 0.

<template>
... lines 2 - 3
<div class="mt-4">
<loading v-show="loading" />
<h5
v-show="!loading && products.length === 0"
class="ml-4"
>
Whoopsie Daisy, no products found!
</h5>
</div>
... lines 14 - 22
</template>
... lines 24 - 47

If that's our situation, print a helpful message. And... there it is! Our snacks page - except for the fact that there are no snacks - works great.

Adding Loading to the Sidebar

The products loading part is now works flawlessly. But there is one other spot that we're loading with Ajax that does not have any loading info: the categories sidebar!

We're actually going to fix this soon by making the categories load instantly. But since they are still loading via Ajax, let's add the loading component there as well. Open up sidebar.vue: this is the component that makes the Ajax request for the categories and renders them in its template.

To do this right, should we add another loading data like we just did in catalog? We totally could! And that's probably a great option. But... I'm going to cheat because I know that my app will never have zero categories. If that ever happened, it would probably mean I accidentally emptied my database. Yikes!

Instead, I am going to use the categories.length to figure out if we're loading. But to be extra organized, let's do this via a computed property called loading. Inside return this.categories.length === 0.

... lines 1 - 48
<script>
... lines 50 - 51
export default {
... lines 53 - 68
computed: {
loading() {
return this.categories.length === 0;
},
},
... lines 74 - 78
};
</script>
... lines 81 - 99

If there are no categories, then we are loading! The nice thing about using a computed property is that it will let us use a simple loading variable in the template. And later, if we did want to change this to data, that would be super easy.

Ok: to use this in the template, first import the loading component: import Loading from '@/components/loading'. Then add the components key with Loading inside.

... lines 1 - 48
<script>
... line 50
import Loading from '@/components/loading';
... line 52
export default {
... line 54
components: {
Loading,
},
... lines 58 - 82
};
</script>
... lines 85 - 103

Finally, up in the template, right after the h5, we'll say <loading with v-show="loading".

<template>
<div :class="[$style.component, 'p-3', 'mb-5']">
<div v-show="!collapsed">
... lines 4 - 7
<loading v-show="loading" />
... lines 9 - 38
</div>
... lines 40 - 47
</div>
</template>
... lines 50 - 105

I love it!

And when we move over to the browser... I'm hoping to see the loading animation right before the categories load. That was super quick! But it was there. We have proper loading on both sides!

Next, I want to start organizing our Ajax calls: we currently make them from inside of sidebar.vue and catalog.vue. That's maybe ok, but I'd like to explore a better way to organize these.

Leave a comment!

4
Login or Register to join the conversation

Why not also throw an error like a good boy inside the catch? `throw new Error(e);`

Reply

Hi Daniel!

I'm not sure I understand your question! Throwing errors inside catch is not something we'd want to do in our application. In our particular case, some times axios throws an error when the ajax request cannot be completed property, so wrapping the calls inside a try...catch is good practice.

Could you may be give us a code sample to illustrate what you mean?

Reply
Abelardo Avatar
Abelardo Avatar Abelardo | posted 2 years ago

A workaround to avoid the "infinite" loading is to show the categories which have items. Those haven't items simply wouldn't appear.
For example, Snacks wouldn't appear at the list of the left.

Reply

Hi Abelardo! Indeed that is a different approach you can take!

By going the route we went, we are able to teach some interesting stuff!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.0
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.1.0
        "eslint-plugin-import": "^2.19.1", // 2.20.2
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.5
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.11
        "vue-loader": "^15.9.1", // 15.9.2
        "vue-template-compiler": "^2.6.11", // 2.6.11
        "webpack-notifier": "^1.6.0" // 1.8.0
    }
}
userVoice