martinho
martinho

Reputation: 1016

Avoid unnecessary http requests on identical images - vuejs

Situation:

In a page, there are several components that receive a list of users. After receiving the list, there's a foreach cycle that calls an aditional component to fetch the user's image. It's possible that the several components may contain the same user, which would mean repeating the exact same http request's to fetch a "repeated image". To avoid these unecessary requests, I set the information of a user has a certain base64 image in the store of vueX, so that I can validate if I already got the image.

Problem: Happens that when the first component makes the request to fetch the image and save it in the store, the remaining components have already been created and as such, the store is still empty and I can't check if I have the image.

Solution: When I create the component, I force the store to exist by using

this.images[this.user.id] = 'reserved'; 

However, I'm not sure if this is the right approach to this situation. Suggestions accepted :'D

Code:

parent component

<template>
    <div class="info-cards">
        <div class="info-users">
            <div class="info-label">{{ $t('global.users') }}</div>
            <div class="info-images"  v-if="users.length > 0">
                <base-users-image
                    v-for="user in users"
                    :key="user.name"
                    :user="user"
                />
            </div>
            <div v-else class="message">{{ $t('global.noUsersRole') }}</div>
        </div>
    </div>
</template>

<script>
    // import components
    const baseUsersImage = () => System.import(/* webpackChunkName: 'usersImage' */ './../../users/baseUsersImage');

    export default {
        props: {
            users: Array,
            packages: Array
        },
        components: {
            baseUsersImage: baseUsersImage
        },
    }
</script>

image component

<template>
    <router-link to="user" class="anchor-image">
        <img v-if="show" :src="image" :alt="user.name" class="image">
        <div v-else class="image-default">t</div>
    </router-link>
</template>

<script>
    // import requests
    import requests from './../../../helpers/requests.js';

    // import store
    import { mapGetters, mapActions } from 'vuex';

    export default {
        props: {
            user: Object
        },
        data() {
            return {
                image: '',
                show: false
            }
        },
        created() {
            if (this.user.avatar) { // check if user has avatar
                if ( this.images[this.user.id] == null) { // check if it already exists in the store
                    this.images[this.user.id] = 'reserved'; // set as reserved in store
                    requests.get(this.user.avatar, { responseType: 'arraybuffer' }) // faz o pedido a API da image
                        .then( (response) => {
                            this.saveImage( { id: this.user.id, url: `data:${response.headers['content-type']};base64,${Buffer.from(response.data, 'binary').toString('base64')}` } );
                        }, error => {
                            console.log(error);
                        });
                }
            }
        },
        methods: {
            ...mapActions({
                saveImage: 'saveImage'
            })
        },
         computed: {
            ...mapGetters({
                images: 'images'
            })
        },
        watch: {
            images:  {
                immediate: true,
                deep: true, // so it detects changes to properties only
                handler(newVal, oldVal) {
                    if ( newVal[this.user.id] !=='reserved'
                        && this.user.avatar
                        && newVal[this.user.id] !== undefined
                    )  {
                        this.image = newVal[this.user.id];
                        this.show = true;
                    }
                }
            }
        }
    }
</script>

store

const state = {
    images: {}
}

const SAVE_IMAGE = (state, payload) => {
    state.images = {
        ...state.images,
        [payload.id] : payload.url
    }
}

const saveImage = ({commit}, payload) => {
    commit('SAVE_IMAGE', payload);
}

Upvotes: 1

Views: 755

Answers (1)

Hammerbot
Hammerbot

Reputation: 16344

Here is what I would do:

First, I would move all the request logic to VueX and keep my component as simple as possible. It should be achievable by this piece of code:

export default {
    props: {
        user: Object
    },
    created () {
        if (this.user.avatar) {
            this.$store.dispatch('fetchImage', this.user.avatar)
        }
    }
}

Then, I would use this simple pattern to organize my store. First, let's take a look at how the state should look:

{
    images: {
        '/users/1/avatar': 'data:png:base64,....', // An image that have been loaded
        '/users/2/avatar': null // An image that is supposed to be loading
    }
}

As you can see, the images object uses images urls as keys and base64 data as value. If the value of the data is null, it means that the image is already loading.

Let's now see how do we write the action to handle that:

const actions = {
    fetchImage ({state, commit}, url) {
        if (typeof state.images[url] !== 'undefined') {
            return null
        }

        commit('setImage', {
            url,
            payload: null
        })

        return requests.get(url, { responseType: 'arraybuffer'}).then(response => {
            commit('setImage', {
                url,
                payload: `data:${response.headers['content-type']};base64,${Buffer.from(response.data, 'binary').toString('base64')}`
            })
        })
    }
}

Look at the first condition. If the image is not undefined in the store, we just don't do anything. Because if the image is not undefined, it means that it is either null (loading) or has a value and is loaded.

Just after this condition, we set the image to null to prevent other components to load the image.

And at the end we load the content of the image, and commit it to the state.

Let's take a look to the template now:

<template>
    <router-link to="user" class="anchor-image">
        <img v-if="$store.state.images[user.avatar]" :src="$store.state.images[user.avatar]" :alt="user.name" class="image">
        <div v-else class="image-default">t</div>
    </router-link>
</template>

In order to check if you should display the image, you just have to use v-if="$store.state.images[user.avatar]". The image will show up as soon as it is loaded.

$store.state.images[user.avatar] will be falsy even if the image is loading (it has the null value.

I hope this can help!

(Here is the complete store:)

const store = {
    state: {
        images: {}
    },
    mutations: {
        setImage (state, image) {
            Vue.set(state.images, image.url, image.payload)
        }
    },
    actions: {
        fetchImage ({state, commit}, url) {
            if (state.images[url] !== undefined) {
                return null
            }

            commit('setImage', {
                url,
                payload: null
            })

            return requests.get(url, { responseType: 'arraybuffer'}).then(response => {
                commit('setImage', {
                    url,
                    payload: `data:${response.headers['content-type']};base64,${Buffer.from(response.data, 'binary').toString('base64')}`
                })
            })
        }
    }
}

Upvotes: 2

Related Questions