Vuex mutation subscription trigger multiple times - vue.js

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.

Related

Is there any way to check the status of props when passing data from parent to child component

I have been troubled by a question for a long time. Now I am using Vue.js to develop a web project. What I want to do is to pass data from parent to child component. However, the child component's main program would run only after the props data was received, due to the async data transmission mechanism. So I would like to know whether these are some ways to check the status of props data in the child component. Therefore I can make sure the subsequent task would run after the data was passed.
For example, a feasible solution is axios.requset({..}).then(res => {..}).
You can use the watchers in your child component. Consider the following parent component:
Vue.component('parent-comp', {
props: ['myProp'],
template: `
<div>
<child-comp my-prop={someAsyncProp} />
</div>
`,
data() {
return {
// Declare async value
someAsyncProp: null
};
},
mounted() {
// Some async computation
axios
.requset({ url: '/get-data' })
.then(res => {
// Set value asynchronously
this.someAsyncProp = res;
});
}
});
Your child component would use watchers to check if data is available:
Vue.component('child-comp', {
props: ['myProp'],
template: '<div></div>',
watch: {
// Watch the property myProp
myProp(newValue, oldValue) {
if (newValue !== null) {
// Do something with the props that are set asynchronously by parent
}
}
}
})

Execute Vuex action before initial routing

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

Vue - Passing component data to view

Hi I'm looking at Vue and building a website with a Facebook login. I have a Facebook login component, which works, although I'm having difficulty making my acquired fbid, fbname, whatever available to my Vues outside the component. Acknowledged this most likely this is a 101 issue, help would be appreciated.
I've tried the global "prototype" variable and didn't manage to get that working. How's this done?
Code below:
main.js
new Vue({
router,
render: h => h(App),
vuetify: new Vuetify()
}).$mount('#app')
App.vue
...
import FacebookComp from './components/FacebookComp.vue'
export default {
name: 'app',
components: {
FacebookComp
},
...
}
Component - FacebookComp.vue
<template>
<div class="facebookcomp">
<facebook-login class="button"
appId="###"
#login="checkLoginState"
#logout="onLogout"
#get-initial-status="checkLoginState">
</facebook-login>
</div>
</template>
<script>
//import facebookLogin from 'facebook-login-vuejs';
//import FB from 'fb';
export default {
name: 'facebookcomp',
data: {
fbuserid: "string"
},
methods: {
checkLoginState() {
FB.getLoginStatus(function(response) {
fbLoginState=response.status;
});
if(fbLoginState === 'connected' && !fbToken){
fbToken = FB.getAuthResponse()['accessToken'];
FB.api('/me', 'get', { access_token: fbToken, fields: 'id,name' }, function(response) {
fbuserid=response.id;
});
}
},
...
}
VIEW - view.vue
...
export default {
name: 'view',
data: function() {
return {
someData: '',
},
mounted() {
alert(this.fbloginId); //my facebook ID here
},
...
}
If you would like to have all these props from FB login available throughout your whole app, I strongly suggest using vuex. If you are not familiar with state management in nowadays SPAs, you basically have global container - state. You can change it only via functions called mutations (synchronous). However, you cannot call them directly from your component. That's what functions called actions (asynchronous) are for. The whole flow is: actions => mutations => state. Once you have saved your desired data in state, you can access it in any of your vue component via functions called getters.
Another option, in case you just need the data visible in your parent only, is to simply emit the data from FacebookComp.vue and listen for event in View.vue. Note that to make this work, FacebookComp.vue has to be a child of View.vue. For more info about implementation of second approach, please look at docs.

Communicate between two components(not related with child-parent)

component 1
getMyProfile(){
this.$root.$emit('event');
console.log("emited")
},
component 2
mounted() {
this.$root.$on('event', () = {
alert("Fired");
}
}
I am trying to alert "fired" of comonent 2 from component 1. But this is not happening. what i am doing wrong. Should i have to add something on main js?
Other than the small typo in your $on, it's not clear what you're doing wrong, as you haven't provided enough context, but here's a working example of exactly what you're trying to do (send and receive an event via the $root element, without instantiating a separate eventbus Vue instance). (Many people do prefer to do the message passing via a separate instance, but it's functionally similar to do it on $root.)
I included a payload object to demonstrate passing information along with the event.
Vue.component('sender', {
template: '<span><button #click="send">Sender</button></span>',
methods: {
send() {
console.log("Sending...")
this.$root.$emit('eventName', {
message: "This is a message object sent with the event"
})
}
}
})
Vue.component('receiver', {
template: '<span>Receiver component {{message}}</span>',
data() {return {message: ''}},
mounted() {
this.$root.$on('eventName', (payload) => {
console.log("Received message", payload)
this.message = payload.message
})
}
})
new Vue({
el: '#app'
});
<script src="https://unpkg.com/vue#latest/dist/vue.min.js"></script>
<div id="app">
<sender></sender>
<receiver></receiver>
</div>
Personally I don't tend to use this pattern much; I find it's better to handle cross-component communication from parent to child as props, and from child to parent as direct $emits (not on $root). When I find I'm needing sibling-to-sibling communication that's generally a sign that I've made some poor architecture choices, or that my app has grown large enough that I should switch over to vuex. Piling all the event messaging into a single location, whether that's $root or an eventbus instance, tends to make the app harder to reason about and debug.
At the very least you should be very explicit in naming your events, so it's easier to tell where they're coming from; event names such as "handleClick" or just "event" quickly become mysterious unknowns.
So what you are looking for is an event bus (global events)
I'd advise considering using vuex anytime you have the need to implement an event bus.
Let's get back to the problem.
Create a file event-bus.js this is what's going to be capturing and distributing events.
import Vue from 'vue'
const EventBus = new Vue()
export default EventBus
Now in your main.js register your event bus
import Vue from 'vue'
import eventBus from './event-bus'
//other imports
Vue.prototype.$eventBus = eventBus
new Vue({
...
}).$mount('#app')
Now you can:
listen for events with this.$eventBus.$on(eventName)
emit events this.$eventBus.$emit(eventName)
in this example i'll bring event from child to parent component with $emit
Child Component:
Vue.component('Child component ', {
methods: {
getMyProfile: function() {
this.$emit('me-event', 'YUP!')
}
},
template: `
<button v-on:click="getMyProfile">
Emmit Event to Parrent
</button>
`
})
Parent Component:
Vue.component('Parent component ', {
methods: {
getEventFromChild: function(event) {
alert(event)
}
}
template: `
<div>
<Child-component v-on:me-event="getEventFromChild" />
</div>
`
})
for example when you have data flow one way from parent to child and you want to bring data from child to parent you can use $emit and bring it from child.. and in the parent you must catch it with v-on directive. (sry form my english)
If component 2 is the parent of the component 1 you could do:
getMyProfile(){
this.$emit('myevent');
console.log("emited")
},
for componant 2 componant like
<componant-2 #myevent="dosomething" ...></componant-2>
and in componant two
methods: {
dosomething() {
console.log('Event Received');
}
}

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.