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?
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)
})
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,
}
})
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>