Giorgio Tempesta
Giorgio Tempesta

Reputation: 2059

How to call a method on a Vue.js component when a particular value is populated, but only once

I'm working on an application that uses Vue.js (currently v 2.6.11), and as the application grows I see a pattern emerge quite often: I want to abstract away some functionality, so I extract a new component, but when I copy some code in the mounted hook from the parent to the child, some of the data that were previously available are now undefined. Sometimes the data comes from a prop, other times it comes from a Vuex store.

I think the issue comes from the fact that, contrarily to what I initially thought, inner components are initialized before outer components.

Here is an example:

Parent component

<template>
    <div class="options-container">
        <features-list />
    </div>
</template>
<script>
import { mapGetters } from 'vuex';
import FeaturesList from '../features/FeaturesList.vue';

export default {
    name: 'ProductComponent',
    components: {
        FeaturesList,
    },
    computed: {
        ...mapGetters('product', [
            'userSelection',
        ]),
    },
    mounted() {
        // here I can call the methods which rely on UserSelections
        // the variable UserSelections has already a value here
        const params = this.getTemplateCollectionParams();
        this.getTemplateAssets(params);
    },
};
</script>

Child component

<template>
    <div class="options">
        <template v-for="key in sortedFeaturesLayout">
            <feature-base-component
                :option="options[key]"
                :key="key"
                v-on="$listeners"
            />
        </template>
    </div>
</template>
<script>
import FeatureBaseComponent from './FeatureBaseComponent.vue';
import { mapGetters } from 'vuex';

export default {
    name: 'FeaturesList',
    components: {
        FeatureBaseComponent,
    },
    computed: {
        ...mapGetters('product', [
            'userSelection',
        ]),
    },
    methods: {
        getTemplateCollectionParams() {
            // ...
        },
        getTemplateAssets(params) {
            // ...
        },
    },
    mounted() {
        // here I have an error because `userSelection` is still empty
        const params = this.getTemplateCollectionParams();
        this.getTemplateAssets(params);
    },
};
</script>

What I usually do is create a watcher on the value and then call the function on the update of the watcher. Since the original code was executing only once I want to check that this method is called only once too, so add a boolean that checks if this code has already been executed.

<template>
    <div class="options">
        <template v-for="key in sortedFeaturesLayout">
            <feature-base-component
                :option="options[key]"
                :key="key"
                v-on="$listeners"
            />
        </template>
    </div>
</template>
<script>
import FeatureBaseComponent from './FeatureBaseComponent.vue';
import { mapGetters } from 'vuex';

export default {
    name: 'FeaturesList',
    components: {
        FeatureBaseComponent,
    },
    data() {
        return {
            isFirstLoad: true,
        };
    },
    computed: {
        ...mapGetters('product', [
            'userSelection',
        ]),
    },
    methods: {
        getTemplateCollectionParams() {
            // ...
        },
        getTemplateAssets(params) {
            // ...
        },
    },
    watch: {
        userSelection() {
            // get gallery
            if (this.isFirstLoad) {
                // here I need the getter userSelection which is empty on `mounted`
                const params = this.getTemplateCollectionParams();
                this.getTemplateAssets(params);
                this.isFirstLoad = false;
            }
        },
    },
};
</script>

I think that what I'm doing is not completely wrong. However, I wonder if there is a cleaner way of executing a method only the first time a prop has a value because sometimes I end up with plenty of booleans, which are only used for this purpose.

Upvotes: 3

Views: 1228

Answers (1)

tony19
tony19

Reputation: 138226

You could alternatively use vm.$watch, which returns an unwatch function that can be called to stop subsequent callbacks:

const unwatch = this.$watch('userSelection', userSelection => {
  //...

  // stop watching
  unwatch()
})

This has the benefits of not requiring an extra flag to check whether the watch handler has already been called, and the watcher is only ever called once.

Edit Unwatching a value in Vue

Upvotes: 1

Related Questions