Prevent $emit from emitting more than once in Vue - vue.js

I have an emit call within my Vue project to update search results, but the emit is being called at least 4 times, because the emit call is defined at various spots, so the data is sometimtes emitted with it and at other spots it is not.
I am using a global bus to perform an emit.
this.$root.bus.$emit('resetValuesInInfiniteLoader', filteredEntities);
this.$root.bus.$on('resetValuesInInfiniteLoader', function (filteredEntities) {});
I tried to name the emits calls differently, and also tried to use a different global vue bus but both options did not really work well.
Any idea how I can do this in an efficient manner so that the $emit is always only called once? How do I need to set it up to ensure that the emit is always only called once? I also tried using $once, which did not work, or tried to destroy the $emit. Can someone give me a small fiddle example or so maybe so I understand how to do this in the right way?

I have found this to be the case also and feel that there are some problems with using it in multiple locations. My understanding is that global event busses are not recommended in most applications as they can lead to a confusing tangle of events. The recommendation is that you use a state management solution like vuex.
But anyway, just a couple of points with your code above. I don't know how you created your bus but I have known to create it as such:
//main.js
const EventBus = new Vue()
Object.defineProperties(Vue.prototype, {
$bus: {
get: function () {
return EventBus
}
}
})
This creates it and makes it global. It can then be triggered in a component or components with:
<button #click="$bus.$emit('my-event')">click</button>
or
methods: {
triggerMyEvent () {
this.$bus.$emit('my-event', { ... pass some event data ... })
}
}
and listened to:
created () {
this.$bus.$on('my-event', ($event) => {
console.log('My event has been triggered', $event)
this.eventItem = 'Event has now been triggered'
//this.$bus.$off('my-event')
})
},
I have found that it works sometimes. I don't know why but it will work then it will trigger several events and I think it is because it isn't finalised or something. You may note I have commented out this.$bus.off which certainly stops it but it then doesn't work again. So I don't know what that's all about.
So there you go, a total non-answer, as in, Yes I've had that too, No I cant fix it.

I went with using vuex store, it seems a lot easier to communicate with any component within the application, has the advantage of global communication, yet does not have the caveat of sending multiple actions such as emit events

Related

Can anyone tell how to speed up my vuejs app?

I am a noob in vuejs. This piece of my code is making my app very slow.
<div v-for="(attribute, i) in attributes" :key="i">
<div>{{ AttributeClicked(attribute) }}</div>
</div>
This is the function:
AttributeClicked(attribute) {
this.$store.commit("entities/Attribute/select", attribute.id);
}
This is the mutation:
mutations: {
select(state, id) {
let selection = Attribute.find(id);
Attribute.update({
where: (a) => a.selected,
data: {
selected: false
}
});
if (selection !== null) {
Attribute.update({
where: id,
data: {
selected: true
}
})
}
},
}
The purpose of this code is to make a webpage like this one https://www.tesla.com/models/design#overview
My objective is for example to show the 5 options below Paint attribute when the page loads.
Can anyone tell me how to speed up this app?
You might need to provide more code or info to get to what you want to be doing, but with the code provided I can see several issues. Maybe understanding the problems will help you get to the solution you are looking for.
You've got a function inside the template, these are fine to pass to event handles such as #click, but they can have a negative effect on performance. Whenever you have the template re-render (which happens when certain data changes) it will re-run the functions. In this case you run the function as many times as you have attributes, and if any if the AttributeClicked method causes a reactivity update to propagate to this template, you will have an endless loop.
looks like you're calling a mutation when an action may be more appropriate. Mutations are strictly for updating state in a synchronous manner. The select mutation does not mutate the state, so it's simply wrong to put it in there. Even though it may work, vuex is a tool that not only stores global state, but also organizes it in an opinionated way. If you're going to go against the intended design, you may find it easier to just avoid using it.
I suspect you may be able to execute AttributeClicked(attribute) one time during component mount.

Vue 3 watch being called before value is fully updated

I'm working on my first Vue 3 (and therefore vuex 4) app coming from a pretty solid Vue 2 background.
I have an component that is loading data via vuex actions, and then accessing the store via a returned value in setup()
setup() {
const store = useStore();
const fancy = computed(() => store.state.fancy);
return { fancy };
},
fancy here is a deeply nested object, which I think might be important.
I then want to run a function based on whenever than value changes, so I have set up a watch in my mounted() lifecycle hook
watch(
this.fancy,
(newValue) => {
for (const spreadsheet in newValue) {
console.log(Object.keys(newValue[spreadsheet].data))
setTimeout(()=>{
console.log(Object.keys(newValue[spreadsheet].data))
},500)
...
I tried using the new onMounted() hook in setup and watchEffect and store.watch instead, but I could not get watch/watchEffect to reliably trigger in the onMounted hook, and watchEffect never seemed to update based on my computed store value. If you have insight into this, that would be handy.
My real issue though is that my watcher gets called seemingly before the value is updated. For instance my code logs out [] for the first set of keys, and then a half second later a full array. If it was some kind of progressive filling in of the data, then I would have expected the watcher to be called again, with a new newValue. I have also tried. all the permutations of flush, deep, and immediate on my watcher to no avail. nextTick()s also did not work. I think this must be related to my lack of understanding in the new reactivity changes, but I'm unsure how to get around it and adding in a random delay in my app seems obviously wrong.
Thank you.

Calling method of another router-view's component / Vue.js

I have two <router-view/>s: main and sidebar. Each of them is supplied with a component (EditorMain.vue and EditorSidebar.vue).
EditorMain has a method exportData(). I want to call this method from EditorSidebar on button click.
What is a good way of tackling it?
I do use vuex, but i don't wanna keep this data reactive since the method requires too much computational power.
I could use global events bus, but it doesn't feel right to use it together with vuex (right?)
I could handle it in root of my app by adding event listener to router-view <router-view #exportClick="handleExportData"> and then target editor component, but it does not feel right as well as later i could need 100 listeners.
Is there any good practice for this? Or did i make some mistakes with the way app is set up? Did is overlooked something in documentation?
After two more years of my adventure with Vue I feel confident enough to answer my own question. It boils down to communication between router views. I've presented two possible solutions, I'll address them separately:
Events bus
Use global events bus (but it doesn't feel right to use it together with vuex)
Well, it may not feel right and it is surely not a first thing you have to think about, but it is perfectly fine use-case for event-bus. The advantage of this solution would be that the components are coupled only by the event name.
Router-view event listeners
I could handle it in root of my app by adding event listener to router-view <router-view #exportClick="handleExportData"> and then target editor component, but it does not feel right as well as later i could need 100 listeners.
This way of solving this problem is also fine, buy it couples components together. Coupling happens in the component containing <router-view/> where all the listeners are set.
Big number of listeners could be addressed by passing an object with event: handler mapping pairs to v-on directive; like so:
<router-view v-on="listeners"/>
...
data () {
return {
listeners: {
'event-one': () => console.log('Event one fired!'),
'event-two': () => console.log('The second event works as well!')
}
}
You could create a plugin for handling exports:
import Vue from 'vue'
ExportPlugin.install = function (Vue, options) {
const _data = new Map()
Object.defineProperty(Vue.prototype, '$exporter', {
value: {
setData: (svg) => {
_data.set('svg', svg)
},
exportData: () => {
const svg = _data.get('svg')
// do data export...
}
}
})
}
Vue.use(ExportPlugin)
Using like:
// EditorMain component
methods: {
setData (data) {
this.$exporter.setData(data)
}
}
// EditorSidebar
<button #click="$exporter.exportData">Export</button>

Reflux setState with a Callback

EDIT AGAIN: Opened an issue with Reflux here: https://github.com/reflux/refluxjs/issues/544
EDIT: Reflux setState does not provide any callback for setState. They require you to use the component lifecycle methods to ensure the state is set prior to running any code. If you ever need to use the reflux setState outside of a component, where you do not have lifecycle methods, you will not be guaranteed the state is set. This is due to how Reflux does their setState. It loops all listening components and calls those components' setState methods. If Reflux were refactored to wait until all the listening components' setState calls complete then call a callback passed into its own setState method, that may work, but it would likely require a large rework of Reflux. I have started using a singleton class to manage some of these variables, as they are fully outside the component lifecycle.
Can you use setState with a callback in ReactNative or is that only in React? I'm using the below syntax and the first debugger is hit, but the second debugger and console log never get hit.
EDIT: After digging some more, it seems this does not occur when using setting the state directly, but only when running it through a reflux store and/or not using a component.
See snack here: https://snack.expo.io/S1dm3eFoM
debugger
this.setState(
params,
() => {
debugger
console.log("CALLIN IT BACK")
}
)
I'm the creator of Reflux's ES6 styled stores/component hookups. Hopefully I can shed some light on this for you.
Here's the important points:
1) Reflux sets its store state immediately upon setState calls.
Reflux's store state doesn't have the same problems as React and doesn't need React's workaround (callback). You are guaranteed that your change is immediately reflected in the store's state, that's why there is not a callback. The very next line of code will reflect the store's new state.
tl;dr, no workaround is required.
// in Reflux stores this works
this.setState({foo:'foo'});
console.log(this.state.foo === 'foo') // true
this.setState({foo:'bar'});
console.log(this.state.foo === 'bar') // true
2) Stores can never depend upon components!
The idea that the setState would give a callback about when the dependent components have all updated their state is a major violation of the single most fundamental of all flux principles: 1 way data flow.
If your store requires knowledge about whether or not components are doing something then you are already doing it wrong, and all the problems you are experiencing are XY problems of fundamentally not following flux in the first place. 1-way data flow is a main flux principle.
And that principle exists for good reason. Flux doesn't require 1:1 mapping of store state properties to component state properties. You can map anything to anything, or even just use the store's state for the building blocks of how you will run your own logic to create completely new state properties on the components. For example having loaded and transitioned as separate properties in store state, but mapping to a loadedAndTransitioned property in one component, and a notLoadedOrTransitioned in another component via your own custom logic. That's a hugely powerful part of flux. But your suggestion would pretty much destroy all that, since Reflux can't map people's custom logic.
1-way data flow must be maintained; Store's must operate the same independently of what components utilize them. Without this, the power of flux falls apart!
Store's listen to actions, components listen to stores, actions are called from wherever. All flux-based data flows from action -> store -> component only.
I've checked the library for the refluxjs and the problem and the workaround are as mentioned below.
Problem
The library provides with a new instance of the setState which is not exactly similar to ReactJS setState, which omits the callback as mentioned in their code below.
/dist/reflux.js
proto.setState = function (obj) {
// Object.assign(this.state, obj); // later turn this to Object.assign and remove loop once support is good enough
for (var key in obj) {
this.state[key] = obj[key];
}
// if there's an id (i.e. it's being tracked by the global state) then make sure to update the global state
if (this.id) {
Reflux.GlobalState[this.id] = this.state;
}
// trigger, because any component it's attached to is listening and will merge the store state into its own on a store trigger
this.trigger(obj);
};
Also as mentioned here in the docs
That store will store its state on a this.state property, and mutate its state via this.setState() in a way that is extremely similar to React classes themselves.
WorkAround
The library provides with the listener functions, which provide us with the callbacks of the setState obj of the ReactJS as mentioned in the below snippet.
/dist/reflux.js
componentDidMount: function() {
var me = this;
_.extend(me, ListenerMethods);
this.listenTo(listenable, function(v) {
me.setState(_.object([key],[v]));
});
},
You can use them in the following way
this.listenTo(action, callback)
Hope it clears the doubts
Edit:
Usage as per the docs
To listen inside of the store
constructor()
{
super();
this.state = {count: 0};
this.listenTo(increment, this.incrementItUp);
}
incrementItUp()
{
var newCount = this.state.count + 1;
this.setState({count: newCount});
}
To listen outside of the store anywhere
// listen directly to an action
myActions.actionName.listen(myCallbackFunc);
// listen to a child action
myActions.load.completed.listen(myCallbackFunc);
Here's the link to the snack with working callbacks based on Promises

How should I handle events in Vuex?

I am used to using a global event bus to handle cross-component methods. For example:
var bus = new Vue();
...
//Component A
bus.$emit('DoSomethingInComponentB');
...
//Component B
bus.$on('DoSomethingInComponentB', function(){ this.doSomething() })
However, I am building a larger project, which requires global state management. Naturally, I want to use Vuex.
While this bus pattern works with Vuex, it seems wrong. I have seen Vuex recommended as a replacement for this pattern.
Is there a way to run methods in components from Vuex? How should I approach this?
Vuex and event bus are two different things in the sense that vuex manages central state of your application while event bus is used to communicate between different components of your app.
You can execute vuex mutation or actions from a component and also raise events from vuex's actions.
As the docs says:
Actions are similar to mutations, the difference being that:
Instead of mutating the state, actions commit mutations.
Actions can contain arbitrary asynchronous operations.
So you can raise an event via bus from actions and you can call an action from any component method.
Using a global event bus is an anti pattern because it becomes very difficult to trace it (where was this event fired from? Where else are we listening to it? etc.)
Why do you want to use a global event bus? Is there some method that you want to trigger in another component? If you use Vuex then all your actions (methods) are in one central state and you can just dispatch your action.
So for example instead of doing this..
// Component A
bus.$emit('DoSomethingInComponentB');
// Component B
bus.$on('DoSomethingInComponentB', function(){ this.doSomething() })
With Vuex you would have the doSomething() function in a central store as an action. Then just make sure to convert you local component data to a global Vuex state data and dispatch that action.
this.$store.dispatch('doSomething')
It may not be directly what you are looking for, but I use watchers to respond to state changes. I come from an Angular background where having side effects respond to actions makes sense to me. To make this work I am having a particular value in the store change and then watch for the value in the relevant component like so:
Vue.component('comp-2', {
...
watch: {
mod1Foo() {
// do something here when the store value of the getter
// mod1/getFoo changes
}
},
computed: {
...mapGetters({
mod1Foo: 'mod1/getFoo'
})
}
});
Here is a full StackBlitz example: https://stackblitz.com/edit/js-gdme1g