skip to Main Content

With the new Vue3 Suspense component, it seems like image loading with skeleton loaders as a default should be quite straight forward. For example, I want to have a default skeleton loader that switches to the real image when it’s been loaded like this:

            <div class="w-40">
                <Suspense>
                    <AsyncImageLoader :image-url="photoUrl" />
                    <template #fallback>
                        <SkeletonLoader shape="circle" class="mx-7 h-[106px]" />
                    </template>
                </Suspense>
            </div>

And then I define my AsyncImageLoader like this:

<template>
    <img
        :src="imageUrl"
        alt="Profile Photo"
        style="filter: invert(0); clip-path: circle(38%)"
    />
</template>

<script lang="ts">
import { defineComponent } from "vue"

export default defineComponent({
    name: "AsyncImageLoader",
    props: {
        imageUrl: {
            type: String,
            required: true
        }
    },
    async setup(props) {}
})
</script>

However this never loads, I believe because there is no promise resolved in the async setup(). If I try to fetch the image URL in the async setup() call I get a CORS error which also makes sense.

Is there a way to easily load images like this? It seems like it should be straight forward with Suspense, but manipulating html tag attributes seem more complicated than just updating the Virtual DOM based on an async action.

2

Answers


  1. Chosen as BEST ANSWER

    After playing around with this for a while, I've determined there isn't an easy way do what I want. Unfortunately Suspense defers component rendering until the async event completes which means waiting on an HTMLEvent for a template tag will never work. I think there are 2 primary options around this:

    1. Perform a fetch() request to grab the image and then update the img element after the fetch request has completed successfully. I think this is the preferred solution but the downside is your server has to serve the image in a CORS friendly way as the Sec-Fetch-Dest header will be empty in the Fetch request. I didn't want to modify the Access-Control-Allow-Origin header to get around this.

    2. Create a new img element and append it to the DOM. This is the method I ended up going for with the code down below:

    <template>
        <div id="render-img-here"></div>
    </template>
    
    <script setup lang="ts">
    import { onMounted } from "vue"
    
    const props = defineProps({
        imageUrl: {
            type: String,
            required: true
        }
    })
    
    const loadImages = async () => {
        return new Promise<HTMLElement>((resolve) => {
            const img = new Image()
            img.src = props.imageUrl
            img.style = "filter: invert(0); clip-path: circle(38%)"
            img.alt = "Profile Photo"
            img.onload = function () {
                console.log("image loaded")
                resolve(img)
            }
        })
    }
    
    const newImage = await loadImages()
    onMounted(() => {
        let container = document.getElementById("render-img-here")
        if (container) {
            console.log(container)
            container.appendChild(newImage)
        }
    })
    </script>
    

    All in all, I'll still probably stick to my first attempt at this and not even use suspense and instead use conditional rendering and a variable which is updated by the v-bind load event. This just makes more sense to me.


  2. To me, you have the good answer to your problem.
    If the methods setup have nothing it say that this is never resolved.

    So you have to resolve your promise, what you want is then the image load display it.
    You need to do a fetch on the url of the image, and this work.

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