I need to check if the image was loaded by the browser and if all data was fetched, then display a content, if not, show <ItemCardSkeleton />
component instead. So if one image is loaded faster than the others, I would want to show the content, and rest would have a skeleton until they are fully loaded one by one.
My attempt: I check if the image was loaded with by adding @load
, so if isLoaded
is set to true
it means it’s loaded. Then I check if the data from API is pending (I passed a prop to the child component) and if the isLoaded
is true
, then display <ItemCardSkeleton />
.
Problem: when I set the throttling to 2G and reload the page, the all of the loaders appear together at once in a few seconds and <ItemCardSkeleton />
loads infinitely, so I never see the real cards appear. Also, I can see {{ isLoaded }}
is always false
in the template.
Parent component ItemSwiper.vue:
<template>
<Swiper>
<template v-for="recipe in storeRecipes.data" :key="recipe.id">
<SwiperSlide class="swiper__slide">
<ItemCard :data="recipe" :pending="storeRecipes.data" />
</SwiperSlide>
<div class="swiper-custom-pagination"></div>
</template>
</Swiper>
</template>
<script setup>
import { onMounted } from 'vue';
import { Swiper, SwiperSlide } from 'swiper/vue';
import { useStoreRecipes } from '@/stores/recipes/storeRecipes.js';
import ItemCard from '@/components/ItemCard.vue';
const storeRecipes = useStoreRecipes();
onMounted(() => {
storeRecipes.loadRecipes();
});
</script>
Child component ItemCard.vue:
<template>
<div class="card">
<div class="card__item">
<ItemCardSkeleton v-if="pending || !isLoaded" />
<template v-else>
<img
class="card__image"
@load="isLoaded = true"
:src="getSrc('.jpg')"
:alt="data.alt"
width="15.625rem" />
<div class="card__content">
<h2 class="card__title">{{ data.title }}</h2>
<p class="card__text">{{ data.text }}</p>
<router-link class="card__link" :to="{ name: 'Home' }"
>View more</router-link
>
</div>
</template>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import ItemCardSkeleton from '@/components/SkeletonLoaders/ItemCardSkeleton.vue';
const props = defineProps(['data', 'pending']);
const isLoaded = ref(false);
const getSrc = ext => {
return new URL(
`../assets/images/recipe/${props.data.image}${ext}`,
import.meta.url
).href;
};
</script>
2
Answers
The issue is that
isLoaded
has to be true for the<img
to be rendered so thatisLoaded
can be set totrue
So, clearly you have an impossible situation since
isLoaded
can’t be set to true until it already IS true.To illustrate what your code is doing, consider this:
As you can see,
isLoaded
can only be set to true whenisLoaded
is already true (the code only runs for 10 seconds,isLoaded
can never be true with this logicTry the following
remove the
@load
from the<img
use
onMounted
to pre-load the image into anew Image
when the image loads, set
isLoaded = true
Something like:
The only issue is, I can’t see how
pending
becomesfalse
, but that’s up to you I guessTo demonstrate with the analogous code from above, some other code needs to set
isLoaded = true
(which is done when thenew Image
loads in the answer, but here in this demo, just in a timeout after 5 seconds)You should change
:pending="storeRecipes.data"
to:pending="storeRecipes.pending"
inItemSwiper.vue
if you intend to pass a boolean indicating whether the data is still loading. Additionally, initializeisLoaded
asfalse
for each new card component.ItemSwiper.vue
ItemCard.vue
Make sure the
getSrc('.jpg')
is providing the correct path. If the path is incorrect or if there’s any issue with the image loading, the@load
event will not be triggered, andisLoaded
will remainfalse
.With these changes, each card will independently control its own loading state, ensuring that the skeleton loader displays until both the data and the image are loaded for each card.