Execute Vuex action before initial routing - vue.js

I have setup Vuex and Vue Router. I want to show a setup page, in case a users account isn't properly setup. This is implemented by making an API call to the backend. The result is stored inside Vuex and accessed using a getter. The action is dispatched in beforeCreate in the root Vuex instance:
new Vue({
router,
store,
render: h => h(App),
beforeCreate() {
this.$store.dispatch("config/get");
}
}).$mount("#app");
However, my router beforeEach never receives true but the getter is definitely returning true as per DevTools:
router.beforeEach((to, next) => {
if (!to.matched.some(record => record.meta.requiresSetup)) {
next();
return;
}
if (!store.getters["config/isConfigured"]) { // Executed every time.
next("/setup");
return;
}
next();
});

Delaying app load
It's not possible to use a lifecycle hook to delay loading even if the hook is marked async and performs an await for some async operation. Hooks aren't intended to allow manipulation, only to give access to the stages.
Still, you can delay the app just by not loading it until the store action completes, but realize this means a blank page for the user, which is a bad user experience. But here's how you can do that:
main.js
const preload = async () => {
await store.dispatch('config/get'); // `await` store action which returns a promise
new Vue({
store,
router,
render: (h) => h(App)
}).$mount("#app");
}
preload();
console.log('LOADING...');
The better way
It's better to dispatch the action and not wait for it. Let App.vue load and use v-if to show a splash screen until some store state isLoading is false:
main.js
store.dispatch('config/get');
new Vue({
store,
router,
render: (h) => h(App)
}).$mount("#app");
App.vue
<template>
<div>
<div v-if="isLoading">
LOADING... <!-- Attractive splash screen here -->
</div>
<div v-else>
<router-view></router-view> <!-- Don't show the router view until ready -->
</div>
</div>
</template>
Remove the navigation guard completely and in App.vue, put a watch on isLoading. Once it's no longer loading, redirect based on the state of the account getter:
computed: {
...mapState(['isLoading'])
},
methods: {
redirect() {
const path = this.$route.path;
if (!this.$store.getters["config/isConfigured"]) {
path !== '/setup' && this.$router.push({ path: '/setup' });
} else {
path !== '/' && this.$router.push({ path: '/' });
}
}
},
watch: {
isLoading(val) {
if (val === false) {
this.redirect();
}
}
}

These lifecycle callbacks have synchronous behaviour.
what you can do is something like
store.dispatch("config/get").then(() => {
router.beforeEach((to, from, next) => {...}
new Vue({...})
})
or you can call it in beforeEach of router and store some state like isLoaded and call store.dispatch only once on firstCall and for the rest of call use stored state

Related

How to use Vue Router scroll behavior with page transition

I'm using Vue Router 4 on an application where the top level RouterView is wrapped in a transition, like so:
<router-view v-slot="{ Component }">
<transition mode="out-in">
<component :is="Component" />
</PageTransition>
</router-view>
When I try to add scroll behavior to the Router, to scroll to a specific element when users navigate back to the index page, the scroll behavior fires during the out phase of the transition, when the index page isn't mounted yet, so the element isn't found.
eg.
const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior (to, from) {
if (to.name === 'index' && isContentPage(from)) {
return { el: '#menu' }
}
return undefined
}
})
I would get a warning in the console: Couldn't find element using selector "#menu" returned by scrollBehavior.
The Vue Router docs on scroll behavior mention that it's possible to work around this issue by hooking up to transition events:
It's possible to hook this up with events from a page-level transition component to make the scroll behavior play nicely with your page transitions, but due to the possible variance and complexity in use cases, we simply provide this primitive to enable specific userland implementations.
But I couldn't figure out what sort of approach it was suggesting, nor could I find any "userland implementations".
Finally I found a solution to this, which was to use a module holding some state—in this case a Promise—to act as the link between the transition and the router.
The module:
let transitionState: Promise<void> = Promise.resolve()
let resolveTransition: (() => void)|null = null
/**
* Call this before the leave phase of the transition
*/
export function transitionOut (): void {
transitionState = new Promise(resolve => {
resolveTransition = resolve
})
}
/**
* Call this in the enter phase of the transition
*/
export function transitionIn (): void {
if (resolveTransition != null) {
resolveTransition()
resolveTransition = null
}
}
/**
* Await this in scrollBehavior
*/
export function pageTransition (): Promise<void> {
return transitionState
}
I hooked up the transition events:
<router-view v-slot="{ Component }">
<transition mode="out-in" #before-leave="transitionOut" #enter="transitionIn">
<component :is="Component" />
</PageTransition>
</router-view>
...and in the Router:
const router = createRouter({
history: createWebHashHistory(),
routes,
async scrollBehavior (to, from) {
if (to.name === 'index' && isContentPage(from)) {
await pageTransition()
return { el: '#menu' }
}
return undefined
}
})
What's more, I actually have a nested RouterView also wrapped in a transition. With that transition extracted to a component, both instances could call transitionOut and transitionIn and it seems to work (though I haven't tested it much for race conditions).
If anyone has found simpler solutions though, I'd be interested to see them.

Vue: How do I wait for a Promise in the component initialization process?

I have several Vue components (realized as .vue files process by Webpack & the Vue Loader) that need to be synchronized (i.e. display / allow interaction of a video stream). Some of them require some initialization effort (i.e. downloading meta data or images).
Can I somehow delay a component until that initialization is ready? Perfect would be if I could wait for a Promise to be fulfilled in a lifecycle hook like beforeCreate.
I know about asynchronous components, but as far as I understand all I could do is to lazy-load that component, and I still would have a way to wait for a certain initialization.
You can do that on route level.
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
doSomeRequests()
.then(() => (next()))
}
}
]
})
doSomeRequests is a function that is doing some async code. When that async code finish
then you call next() to allow vue router to enter the component which corresponds to the url. In the example the component Foo
My suggestion would be to show a loader in the component.
<template>
<!-- code -->
<LoaderComponent :loading="loading" />
<!-- code -->
</template>
And the script part:
<script>
export default {
// code
data: () => ({
loading: false
}),
created () {
this.loading = true
doSomeRequests()
.then(<!-- code -->)
.finally(() => (this.loading = false))
}
}
</script>

Vuex mutation subscription trigger multiple times

I'm using vue CLI and I created multiple components
app.vue
import home_tpl from './home.vue';
new vue({
el : '#app',
components : { home_tpl },
created(){
this.$store.subscribe((mutation) => {
switch(mutation.type){
case 'listing':
alert();
break;
});
}
})
and then I have also a listener to home.vue
home.vue
export default{
created(){
this.$store.subscribe((mutation) => {
switch(mutation.type){
case 'listing':
alert();
break;
});
}
}
The problem is when I do this.$store.commit('listing',1); this this.$store.subscribe((mutation) => { trigger twice which is the expected behavior since I listen to the event twice from different file, is there a way to make it trigger once only per component?
The reason I call mutation listener to home.vue is that because there's an event that I want to run specifically to that component only.
You sample code listen to listing change for both app.vue and home.vue, but according to your posts, it seems they are interested in different kind of changes?
As commented, watch should be a better approach if you are only interested in a few changes rather than all the changes of the store. Something like:
// home.vue
new vue({
el : '#app',
components : { home_tpl },
created(){
this.$store.watch((state, getters) => state.stateHomeIsInterested, (newVal, oldVal) => {
alert()
})
}
})
// app.vue
export default{
created(){
this.$store.watch((state, getters) => state.stateAppIsInterested, (newVal, oldVal) => {
alert()
})
}
}
The difference is:
the subscribe callback will be called whenever there is a mutation in the store (in your case this may waste some unnecessary callback invocations). The callback method receives the mutation and the updated state as arguments
the watch will only react on the changes to the return value of the getter defined in its first argument, and the callback receives the new value and the old value as arguments. You can watch multiple states if required.

Vue-router component reusing

I would like to know how can I stop component reusing in Vue-router.
I'm building a simple page application and I am unable to update data after clicking the same link twice.
Is it possible to somehow force reloading or what are the best practices in my situation?
Use the key attribute on router-view set to current url. It's built in, so no need to write any code.
<router-view :key="$route.fullPath"></router-view>
Vue Router reuses the same component therefore the mounted hook won't be called. As stated in the documentation:
The same component instance will be reused [...] the lifecycle hooks of the component will not be called.
If you want to update the data you have two options:
Watch the $route object
const User = {
template: '...',
watch: {
'$route' (to, from) {
// react to route changes...
}
}
}
Use the beforeRouteUpdate navigation guard
const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
// react to route changes...
// don't forget to call next()
}
}
For a more detailed explanation you can check the section Reacting to Param Changes of the Vue Router documentation: https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes.
One way to do this is to put a key on the router-view and append a timestamp querystring to your router-link
const Home = {
template: '<div>Home</div>',
created() {
console.log('This should log everytime you click home.');
},
};
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
]
});
new Vue({
router,
el: '#app',
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<router-link :to="`/?q=${Date.now()}`">/home</router-link>
<router-view :key="$route.fullPath"></router-view>
</div>
One reason not to do it this way is because it'll force rerenders on components that you may want to be reused such as on routes like
/posts/1
/posts/2

Vuex accessing state BEFORE async action is complete

I'm having issues where a computed getter accesses the state before it is updated, thus rendering an old state. I've already tried a few things such as merging mutations with actions and changing state to many different values but the getter is still being called before the dispatch is finished.
Problem
State is accessed before async action (api call) is complete.
Code structure
Component A loads API data.
User clicks 1 of the data.
Component A dispatches clicked data (object) to component B.
Component B loads object received.
Note
The DOM renders fine. This is a CONSOLE ERROR. Vue is always watching for DOM changes and re-renders instantly. The console however picks up everything.
Goal
Prevent component B (which is only called AFTER component) from running its computed getter method before dispatch of component A is complete.
Store.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
searchResult: {},
selected: null,
},
getters: {
searchResult: state => {
return state.searchResult;
},
selected: state => {
return state.selected;
},
},
mutations:{
search: (state, payload) => {
state.searchResult = payload;
},
selected: (state, payload) => {
state.selected = payload;
},
},
actions: {
search: ({commit}) => {
axios.get('http://api.tvmaze.com/search/shows?q=batman')
.then(response => {
commit('search', response.data);
}, error => {
console.log(error);
});
},
selected: ({commit}, payload) => {
commit('selected', payload);
},
},
});
SearchResult.vue
<template>
<div>
//looped
<router-link to="ShowDetails" #click.native="selected(Object)">
<p>{{Object}}</p>
</router-link>
</div>
</template>
<script>
export default {
methods: {
selected(show){
this.$store.dispatch('selected', show);
},
},
}
</script>
ShowDetails.vue
<template>
<div>
<p>{{Object.name}}</p>
<p>{{Object.genres}}</p>
</div>
</template>
<script>
export default {
computed:{
show(){
return this.$store.getters.selected;
},
},
}
</script>
This image shows that the computed method "show" in file 'ShowDetails' runs before the state is updated (which happens BEFORE the "show" computed method. Then, once it is updated, you can see the 2nd console "TEST" which is now actually populated with an object, a few ms after the first console "TEST".
Question
Vuex is all about state watching and management so how can I prevent this console error?
Thanks in advance.
store.dispatch can handle Promise returned by the triggered action handler and it also returns Promise. See Composing Actions.
You can setup your selected action to return a promise like this:
selected: ({commit}, payload) => {
return new Promise((resolve, reject) => {
commit('selected', payload);
});
}
Then in your SearchResults.vue instead of using a router-link use a button and perform programmatic navigation in the success callback of your selected action's promise like this:
<template>
<div>
//looped
<button #click.native="selected(Object)">
<p>{{Object}}</p>
</button>
</div>
</template>
<script>
export default {
methods: {
selected(show){
this.$store.dispatch('selected', show)
.then(() => {
this.$router.push('ShowDetails');
});
},
},
}
</script>
You can try to use v-if to avoid rendering template if it is no search results
v-if="$store.getters.searchResult"
Initialize your states.
As with all other Vue' data it is always better to initialize it at the start point, even with empty '' or [] but VueJS (not sure if Angular or React act the same, but I suppose similar) will behave much better having ALL OF YOUR VARIABLES initialized.
You can define initial empty value of your states in your store instance.
You will find that helpful not only here, but e.g. with forms validation as most of plugins will work ok with initialized data, but will not work properly with non-initialized data.
Hope it helps.