Using the mitt-library in Vue with Webpack hot-reload causes problems? - vue.js

In my Vue3 app, I'm using the mitt eventbus library to emit and receive events between components.
I put this in onMounted of a list component that needs to refresh:
mitt.on("list_refresh", (evt) => {
refresh();
});
In another component that contains the list-component as a child (or grandchild), I do this in a method:
mitt.emit("list_refresh", {});
This works ok, but while developing with hot-reload on, the events seem to be emitted multiple times, as if they're created extra each time the app reloads, instead of overwriting the old ones.
When I reload the entire page in the browser, it works fine again.
Any idea to prevent this?

It looks like your component is missing a corresponding off() call to remove the event listener. During hot reload, the current component instances unmount, and new ones mount; so if you're not removing current event listeners, you'll just pile on new event listeners. To resolve the issue, use the onUnmounted hook to remove the event listener when the component is removed from the DOM.
Also, make sure to pass cached function references (instead of inline functions) to mitt.on() and mitt.off() to ensure the given event listener lookup succeeds in mitt.off():
// mitt.on('list_refresh', () => refresh()) ❌
mitt.on('list_refresh', refresh) ✅
mitt.off('list_refresh', refresh)
Your setup() should look similar to this:
import { onMounted, onUnmounted } from 'vue'
export default {
setup() {
const refresh = () => { /*...*/ }
onMounted(() => mitt.on('list_refresh', refresh))
onUnmounted(() => mitt.off('list_refresh', refresh)) 👈
}
}

Related

Vue.js: Global event bus is failing to catch the event

I am trying to create a global event bus to be emitted from Source.vue to be caught in Destination.vue. The latter component has to run a function when the event is caught.
Source.vue:
.then(() => {
eventBus.$emit("fireMethod");
})
Destination.vue:
updated() {
eventBus.$on("fireMethod", () => {
this.reqMethod();
});
}
main.js:
export const eventBus = new Vue();
I have also imported the main.js file in both of the components. But that doesn't seem to work as the event is not caught at all. What's wrong here?
Thanks in advance
The updated hook runs after data changes on your component and the DOM re-renders. Hence on the initial render your event listener is not registered as the event is not triggered at that time.
Kindly go through this fiddle to get a clear idea about vue components life cycle.
Hence in Destination.vue:
Your event hook should be mounted and not updated.
mounted() {
eventBus.$on("fireMethod", () => {
this.reqMethod();
});
}
To register global event bus you can use this in your main.js
Vue.prototype.$eventHub = new Vue(); //global event bus
And then in your components use it like
//on emit
this.$eventHub.$on('your-event',function(){});
//to emit
this.$eventHub.$emit('your-event',data);

Dynamically listen to custom event on parent component

In my custom library I have several components which run ajax requests, and when a given request fails the component emits an error event.
Then, in my main component I want to listen to all error events emmited and run the method handleErrors, but for that I have to go on every component and add #error="handleErrors".
Is there a way to configure my main component to catch error events dynamically and call handleErrors without going on each component and adding it? Preferrably changes to the main component only.
You can use the EventBus system in Vue instance. Actually EventBus is a different Vue instance your Main Vue instance. You make your own Event bus system.$emit, $on and $off events.
event-bus.js
import Vue from 'vue';
export const EventBus = new Vue();
and now you are ready to use.
some-component.vue
// Import the EventBus.
import { EventBus } from './event-bus.js';
// Send the event on a channel (eventName) with a payload (the click count.)
EventBus.$emit('eventName', this.clickCount);
other-component.vue
// Import the EventBus.
import { EventBus } from './event-bus.js';
// Listen for the eventName event and its payload.
EventBus.$on('eventName', clickCount => {
console.log(clickCount)
});
// Stop listening.
EventBus.$off('eventName');
more information and example
https://alligator.io/vuejs/global-event-bus/
Figured a way to do it without refactoring the individual components: just injected the listener in every component using a mixin.
The result:
// Before starting up the main instance:
Vue.mixin({
created: function() {
this.$on('error', function(error) {
// Then I handled the errors here.
);
}
});

How to use vuejs global event to notify after redirection

I have created a global vuejs event named EvenBus in a file called app.js as follows:
window.EventBus = new Vue();
After deleting a person from a database, I redirect to the home page and I emit an event called deleted like so:
import {app} from '../app';
export default {
methods: {
deleteMe: function () {
axios.delete('/api/persons/' + this.person.id)
.then(response => {
window.location.href = '/home';
EventBus.$emit('deleted', {
notification_msg: 'This person has been successfully deleted.',
});
})
},
}
on the page home, I have inserted a tag <notify></notify> linked to a component called notify.vue.
In this component, I added the following script:
import {app} from '../app';
export default {
/* ... */
mounted() {
let that = this;
console.log(EventBus);
EventBus.$on('deleted', function(data){
alert('person deleted!!' + data.notification_msg);
that.msg_text = data.notification_msg;
});
},
}
When the delete happens, I get successfully redirected to the home page, but nothing happens there. The alert('person deleted!!...') never shows up.
The code is executed with no error.
Am I missing something to make my component listen to the emitted event?
EDIT
The line console.log(EvenBus); written in the notify.vue file shows that there is a event called 'deleted' (c.f. printscreen below)
The issue here is that when this line of code is executed,
window.location.href = '/home';
the page you are on is replaced with the /home page. So, it's possible it never even gets to the line that emits the event and if it does, the object that listens to the event is gone when the page is loaded.
You might want to look into using VueRouter so that the page isn't destroyed. Other than that, possibly tell the server when you are redirecting that it needs to show the notification when the page is loaded.

vue 2 lifecycle - how to stop beforeDestroy?

Can I add something to beforeDestroy to prevent destroying the component? ?
Or is there any way to prevent destroying the component ?
my case is that when I change spa page by vue-route, I use watch route first, but I found that doesn't trigger because the component just destroy..
As belmin bedak commented you can use keep-alive
when you use keep-alive two more lifecycle hooks come into action, they are activated and deactivated hooks instead of destroyed
The purpose of keep-alive is to cache and to not destroy the component
you can use include and exclude atteibutes of the keep-alive element and mention the names of the components that shoulb be included to be cached and be excluded from caching. Here is documentation
in case you want to forecefully destroy the component even if its cached you can use vm.$destroy() here
Further you can console.log in all the lifecycle hooks and check which lifecycle hook is being called
You can use vue-route navigation-guards, so if you call next(false) inside the hook, navigation will be aborted.
router.afterEach((to, from) => {
if(your condition){
next(false) //this will abort route navigation
}
})
According to this source: https://router.vuejs.org/guide/advanced/navigation-guards.html
I suggest you to do something like this with your Vue router:
const router = new VueRouter({ }); // declare your router with params
router.beforeEach((to, from, next) => {
if(yourCondition){
next(false); // prevent user from navigating somewhere
} else {
return next(); // navigate to next "page" as usual
}
});
This will prevent destroying your Vue instance on your declared condition, and it will also prevent user from navigating to another page.
Although I would consider #Vamsi Krishna "keep-alive" answer to be the proper "VueJS way" to solve this issue, I was not willing to refactor part of my code for it.
I also couldn't use the Vue router navigation guard "as-is" because in the case of beforeRouterLeave, even though using next(false) prevented the route from continuing, the component in Vue was ALREADY destroyed. Any state I had that wasn't saved would be lost, which defeats the purpose of cancelling the route change.
This wasn't what I wanted, as I needed the state of the form/settings in the component to remain (the component reloaded itself and kept the same route).
So I came up with a strategy that still used a navigation guard, but also cached any form changes/settings I had in the component in-memory, eg. I add a beforeRouteLeave hook in the component:
beforeRouteLeave (to, from, next) {
if (!this.isFormDirty() || confirm('Discard changes made?')) {
_cachedComponentData = null // delete the cached data
next()
} else {
_cachedComponentData = this.componentData // store the cached data based on component data you are setting during use of the component
next(false)
}
}
Outside the Vue component, I initialize _cachedComponentData
<script>
let _cachedComponentData = null
module.exports = {
...component code here
}
<script>
Then in the created or mounted life cycle hooks, I can set the _cachedComponentData to "continue where the user left off" in the component:
...
if (_cachedComponentData) {
this.componentData = _cachedComponentData
}
...

Vuex and Electron carrying state over into new window

I'm currently building an application using Electron which is fantastic so far.
I'm using Vue.js and Vuex to manage the main state of my app, mainly user state (profile and is authenticated etc...)
I'm wondering if it's possible to open a new window, and have the same Vuex state as the main window e.g.
I currently show a login window on app launch if the user is not authenticated which works fine.
function createLoginWindow() {
loginWindow = new BrowserWindow({ width: 600, height: 300, frame: false, show: false });
loginWindow.loadURL(`file://${__dirname}/app/index.html`);
loginWindow.on('closed', () => { loginWindow = null; });
loginWindow.once('ready-to-show', () => {
loginWindow.show();
})
}
User does the login form, if successful then fires this function:
function showMainWindow() {
loginWindow.close(); // Also sets to null in `close` event
mainWindow = new BrowserWindow({width: 1280, height: 1024, show: false});
mainWindow.loadURL(`file://${__dirname}/app/index.html?loadMainView=true`);
mainWindow.once('resize', () => {
mainWindow.show();
})
}
This all works and all, the only problem is, the mainWindow doesn't share the same this.$store as its loginWindow that was .close()'d
Is there any way to pass the Vuex this.$store to my new window so I don't have to cram everything into mainWindow with constantly having to hide it, change its view, plus I want to be able to have other windows (friends list etc) that would rely on the Vuex state.
Hope this isn't too confusing if you need clarification just ask. Thanks.
Although I can potentially see how you may do this I would add the disclaimer that as you are using Vue you shouldn't. Instead I would use vue components to build these seperate views and then you can achieve your goals in an SPA. Components can also be dynamic which would likely help with the issue you have of hiding them in your mainWindow, i.e.
<component v-bind:is="currentView"></component>
Then you would simply set currentView to the component name and it would have full access to your Vuex store, whilst only mounting / showing the view you want.
However as you are looking into it I believe it should be possible to pass the values of the store within loginWindow to mainWindow but it wouldn't be a pure Vue solution.
Rather you create a method within loginWindows Vue instance that outputs a plain Object containing all the key: value states you want to pass. Then you set the loginWindows variable to a global variable within mainWindow, this would allow it to update these values within its store. i.e.
# loginWindow Vue model
window.vuexValuesToPass = this.outputVuexStore()
# mainWindow
var valuesToUpdate = window.opener.vuexValuesToPass
then within mainWindows Vue instance you can set up an action to update the store with all the values you passed it
Giving the fact that you are using electron's BrowserWindow for each interaction, i'd go with ipc channel communication.
This is for the main process
import { ipcMain } from 'electron'
let mainState = null
ipcMain.on('vuex-connect', (event) => {
event.sender.send('vuex-connected', mainState)
})
ipcMain.on('window-closed', (event, state) => {
mainState = state
})
Then, we need to create a plugin for Vuex store. Let's call it ipc. There's some helpful info here
import { ipcRenderer } from 'electron'
import * as types from '../../../store/mutation-types'
export default store => {
ipcRenderer.send('vuex-connect')
ipcRenderer.on('vuex-connected', (event, state) => {
store.commit(types.UPDATE_STATE, state)
})
}
After this, use the store.commit to update the entire store state.
import ipc from './plugins/ipc'
var cloneDeep = require('lodash.clonedeep')
export default new Vuex.Store({
modules,
actions,
plugins: [ipc],
strict: process.env.NODE_ENV !== 'production',
mutations: {
[types.UPDATE_STATE] (state, payload) {
// here we update current store state with the one
// set at window open from main renderer process
this.replaceState(cloneDeep(payload))
}
}
})
Now it remains to send the vuex state when window closing is fired, or any other event you'd like. Put this in renderer process where you have access to store state.
ipcRenderer.send('window-closed', store.state)
Keep in mind that i've not specifically tested the above scenario. It's something i'm using in an application that spawns new BrowserWindow instances and syncs the Vuex store between them.
Regards
GuyC's suggestion on making the app totally single-page makes sense. Try vue-router to manage navigation between routes in your SPA.
And I have a rough solution to do what you want, it saves the effort to import something like vue-router but replacing components in the page by configured routes is always smoother than loading a new page: when open a new window, we have its window object, we can set the shared states to the window's session storage (or some global object), then let vuex in the new window to retrieve it, like created() {if(UIDNotInVuex) tryGetItFromSessionStorage();}. The created is some component's created hook.