skip to Main Content

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


  1. The issue is that isLoaded has to be true for the <img to be rendered so that isLoaded can be set to true

    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:

    let isLoaded = false;
    let pending = true;
    function x(n=0) {
        console.log(JSON.stringify({n, pending, isLoaded}));
        if (n > 9) return;
        if (pending || !isLoaded) {
            setTimeout(() => {
                pending = false;
                x(n+1);
            }, 1000);
        } else {
            isLoaded = true;
        }
    }
    x();

    As you can see, isLoaded can only be set to true when isLoaded is already true (the code only runs for 10 seconds, isLoaded can never be true with this logic

    Try the following

    remove the @load from the <img

    use onMounted to pre-load the image into a new Image

    when the image loads, set isLoaded = true

    Something like:

    <template>
        <div class="card">
            <div class="card__item">
                <ItemCardSkeleton v-if="pending || !isLoaded" />
                <template v-else>
                    <img
                        class="card__image"
                        :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, onMounted } 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;
    };
    onMounted(() => {
        const img = new Image;
        img.onload = () => isLoaded.value = true;
        img.src = getSrc('.jpg');
    });
    </script>
    

    The only issue is, I can’t see how pending becomes false, but that’s up to you I guess

    To demonstrate with the analogous code from above, some other code needs to set isLoaded = true (which is done when the new Image loads in the answer, but here in this demo, just in a timeout after 5 seconds)

    let isLoaded = false;
    let pending = true;
    function x(n=0) {
        console.log(JSON.stringify({n, pending, isLoaded}));
        if (n > 9) return;
        if (pending || !isLoaded) {
            setTimeout(() => {
                pending = false;
                x(n+1);
            }, 1000);
        } else {
            isLoaded = true;
        }
    }
    x();
    setTimeout(() => {
        isLoaded = true;
    }, 5000)
    Login or Signup to reply.
  2. You should change :pending="storeRecipes.data" to :pending="storeRecipes.pending" in ItemSwiper.vue if you intend to pass a boolean indicating whether the data is still loading. Additionally, initialize isLoaded as false for each new card component.

    ItemSwiper.vue

    <template>
        <Swiper>
            <template v-for="recipe in storeRecipes.data" :key="recipe.id">
                <SwiperSlide class="swiper__slide">
                    <!-- Pass the boolean flag indicating loading state -->
                    <ItemCard :data="recipe" :pending="storeRecipes.pending" />
                </SwiperSlide>
            </template>
        </Swiper>
    </template>
    

    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" <!-- This should set isLoaded to true when image loads -->
              :src="getSrc('.jpg')"
              :alt="data.alt"
              width="15.625rem" />
            <!-- ... -->
          </template>
        </div>
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    import ItemCardSkeleton from '@/components/SkeletonLoaders/ItemCardSkeleton.vue';
    
    const props = defineProps(['data', 'pending']);
    
    // Initialize as false for each new component
    const isLoaded = ref(false);
    
    // ...
    </script>
    

    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, and isLoaded will remain false.

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search