Search code examples
javascriptvue.jsvuejs2vue-multiselect

Possible/How to use a VueJS component multiple times but only execute its created/mounted function once?


I am trying to create a VueJS component that does the following: 1) download some data (a list of options) upon mounted/created; 2) display the downloaded data in Multiselct; 3) send selected data back to parent when user is done with selection. Something like the following:

<template>
    <div>
        <multiselect v-model="value" :options="options"></multiselect>
    </div>
</template>

<script>
import Multiselect from 'vue-multiselect'
export default {
    components: { Multiselect },
    mounted() {
        this.getOptions();
    },
    methods:{
        getOptions() {
            // do ajax
            // pass response to options
        }
    },
    data () {
        return {
            value: null,
            options: []
        }
    }
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>

This is mostly straightforward if the component is only called once in a page. The problem is I may need to use this component multiple times in one page, sometimes probably 10s of times. I don't want the function to be called multiple times:

this.getOptions();

Is there a way to implement the component somehow so no matter how many times it is used in a page, the ajax call will only execute once?

Thanks in advance.

Update: I assume I can download the data in parent then pass it as prop if the component is going to be used multiple times, something like the following, but this defies the purpose of a component.

    props: {
        optionsPassedByParents: Array
    },
    mounted() {
        if(this.optionsPassedByParents.length == 0)
            this.getOptions();
        else
            this.options = this.optionsPassedByParents;
    },

Solution

  • The simple answer to your question is: you need a single place in charge of getting the data. And that place can't be the component using the data, since you have multiple instances of it.

    The simplest solution is to place the contents of getOptions() in App.vue's mounted() and provide the returned data to your component through any of these:

    • a state management plugin (vue team's recommendation: pinia)
    • props
    • provide/inject
    • a reactive object (export const store = reactive({/* data here */})) placed in its own file, imported (e.g: import { store } from 'path/to/store') in both App.vue (which would populate it when request returns) and multiselect component, which would read from it.

    If you don't want to request the data unless one of the consumer components has been mounted, you should use a dedicated controller for this data. Typically, this controller is called a store (in fairness, it should be called storage):

    • multiselect calls an action on the store, requesting the data
    • the action only makes the request if the data is not present on the store's state (and if the store isn't currently loading the data)
    • additionally, the action might have a forceFetch param which allows re-fetching (even when the data is present in state)

    Here's an example using pinia (the official state management solution for Vue). I strongly recommend going this route.


    And here's an example using a reactive() object as store.
    I know it's tempting to make your own store but, in my estimation, it's not worth it. You wouldn't consider writing your own Vue, would you?

    const { createApp, reactive, onMounted, computed } = Vue;
    
    const store = reactive({
      posts: [],
      isLoading: false,
      fetch(forceFetch = false) {
        if (forceFetch || !(store.posts.length || store.isLoading)) {
          store.isLoading = true;
          try {
            fetch("https://jsonplaceholder.typicode.com/posts")
              .then((r) => r.json())
              .then((data) => (store.posts = data))
              .then(() => (store.isLoading = false));
          } catch (err) {
            store.isLoading = false;
          }
        }
      },
    });
    
    app = createApp();
    
    app.component("Posts", {
      setup() {
        onMounted(() => store.fetch());
        return {
          posts: computed(() => store.posts),
        };
      },
      template: `<div>Posts: {{ posts.length }}</div>`,
    });
    
    app.mount("#app");
    <script src="https://unpkg.com/vue/dist/vue.global.prod.js"></script>
    <div id="app">
      <Posts v-for="n in 10" :key="n" />
    </div>

    As you can see in network tab, in both examples data is requested only once, although I'm mounting 10 instances of the component requesting the data. If you don't mount the component, the request is not made.