Search code examples
vuejs3es6-promisepinia

How to signal a view when pinia store is fully loaded?


We have a item view whose contents depends on a store with is loaded from a (rather slow) api. We found that the view is mounted before the store is fully loaded which is ok but how can a store declare its state loaded? We tried this in the store:

    async function initialize() {
        Promise.all([fetchEquipmentShortlist(), fetchEquipmentList()])
        .then(() => {
            console.log('initialized');

        })
    }

where the initialize() method is called in main.js and the two functions are async/await fetch() functions. What could we put in the .then() resolve function to signal the view to start displaying the contents?

main.js:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { de, en } from 'vuetify/locale'

import App from './App.vue'
import router from './router'
import './assets/index.css'

import { useSystemStore } from "@/store/SystemStore.js";
import { useEquipmentStore } from "@/store/EquipmentStore.js";

const mountEl = document.querySelector("#app")
const app = createApp(App, { ...mountEl.dataset })

//Vuetify
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'

const vuetify = createVuetify({
  components,
  directives,
  locale: {
    messages: { de, en }
  }
})

app
  .use(createPinia())
  .use(vuetify)
  .use(router)

// Fetch settings.json
fetch(import.meta.env.BASE_URL + 'config/settings.json')
  .then((response) => response.json())
  .then((config) => {
    for (const key in config) {
      app.provide(key, config[key])
    }
    const equipmentStore = useEquipmentStore()
    equipmentStore.setApiUrl(config.apiUrl)
    equipmentStore.initialize()
    app.mount('#app')
  })
  .catch((error) => {
    console.error('Error:', error)
  })

EquipmentStore

import { ref, computed, watch } from 'vue'
import { defineStore, storeToRefs } from 'pinia'

export const useEquipmentStore = defineStore('EquipmentStore', () => {
    // State variables
    const apiUrl = ref('inject me')
    const equipmentList = ref([])

    const setApiUrl = (apiUrlValue) => {
        apiUrl.value = apiUrlValue
        return this;
    }

    // Fetch the equipment list when the component is created
    const fetchEquipmentList = async () => {
        console.log('fetchEquipmentList')
        const url = `${apiUrl.value}Equipment`

        try {
            const response = await fetch(url);

            if (!response.ok) {
                console.error('Failed to fetch equipment shortlist');
                return;
            }

            const result = await response.json();
            equipmentList.value = result.resources;
        } catch (error) {
            console.error('Error fetching equipment shortlist:', error);
        }
    }
 
    async function initialize() {
        Promise.all([fetchEquipmentList()])
        .then(() => {
            console.log('initialized');
        })
    }


    return {
        apiUrl,
        equipmentList,
        initialize,
        setApiUrl,
    }
})

Solution

  • I've created a simple and stripped down version of your provided example. All it does is add an extra isLoading ref in your store that can be used to display a loading state. E.g:

    const { createApp, ref } = Vue;
    const { createPinia, defineStore } = Pinia;
    
    // Store.
    const useEquipmentStore = defineStore( 'EquipmentStore', () => {
      const isLoading = ref( false );
      const equipmentList = ref( [] );
    
      const fetchEquipmentList = async () => {
        isLoading.value = true;
    
        try {
          // Mock of equipment list fetch.
          await new Promise( resolve => setTimeout( resolve, 2000 ) );
          equipmentList.value = [ 'foo', 'bar', 'baz' ];
        }
        catch ( error ) {
          console.error( 'Error fetching equipment shortlist:', error );
        }
        finally {
          isLoading.value = false;
        }
      };
    
      async function initialize() {
        await fetchEquipmentList();
      }
    
      return {
        equipmentList,
        initialize,
        isLoading,
      };
    } );
    
    // App.
    const app = createApp( {
      setup() {
        const store = useEquipmentStore();
        return { store };
      },
      mounted() {
        this.store.initialize();
      },
      template: `
        <div>
          <div v-if="store.isLoading">
            Fetching data...
          </div>
    
          <div v-else>
            Data received: {{ store.equipmentList }}
          </div>
        </div>`,
    } );
    
    const pinia = createPinia();
    
    app.use( pinia );
    app.mount( '#app' );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.5.4/vue.global.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.iife.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/pinia.iife.min.js"></script>
    
    <div id="app"></div>

    Alternatively you can use a status instead of isLoading to differentiate between muliple states (this is also done in Nuxt useFetch for example).

    const { createApp, ref } = Vue;
    const { createPinia, defineStore } = Pinia;
    
    // Store.
    const useEquipmentStore = defineStore( 'EquipmentStore', () => {
      const status = ref( 'idle' );
      const equipmentList = ref( [] );
    
      const fetchEquipmentList = async () => {
        status.value = 'loading';
    
        try {
          // Mock of equipment list fetch.
          await new Promise( resolve => setTimeout( resolve, 2000 ) );
          equipmentList.value = [ 'foo', 'bar', 'baz' ];
          
          // uncomment next line to simulate an error.
          // throw new Error( 'Failed retrieving equipment list.' );
          
          status.value = 'success';
        }
        catch ( error ) {
          status.value = 'error';
        }
      };
    
      async function initialize() {
        await fetchEquipmentList();
      }
    
      return {
        equipmentList,
        initialize,
        status,
      };
    } );
    
    // App.
    const app = createApp( {
      setup() {
        const store = useEquipmentStore();
        return { store };
      },
      mounted() {
        this.store.initialize();
      },
      template: `
        <div>
          <p>Status: {{ store.status }}</p>
          
          <div v-if="store.status === 'loading'">
            Fetching data...
          </div>
    
          <div v-else-if="store.status === 'success'">
            Data received: {{ store.equipmentList }}
          </div>
          
          <div v-else-if="store.status === 'error'">
            <p>Something went wrong!</p>
          </div>
        </div>`,
    } );
    
    const pinia = createPinia();
    
    app.use( pinia );
    app.mount( '#app' );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.5.4/vue.global.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.iife.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/pinia.iife.min.js"></script>
    
    <div id="app"></div>