Laravel echo listens multiple times - laravel-echo

I have created a simple chat app wherein a user may send a message privately. Now, while the page is on first load (mounted), the console logs only one line. If I click on other links (not hard refresh) and go back to the page, the console logs multiple times.
Using ReactJS
useEffect(() => {
window.Echo.private(`chat`).listen('PrivateMessageEvent', e => {
console.log(e) // this logs multiple times
});
}, []);
If using VueJS
mounted() {
window.Echo.private(`chat`).listen('PrivateMessageEvent', e => {
console.log(e) // this logs multiple times
});
}
Example:
When I visit page /first-link, this logs only one line.
Then, if go to /second-link (<router-link> for vuejs / <Link /> for reactjs) , then /third-link and go back to /first-link, the console logs multiple times.
I notice that the Echo listens multiple times. How can I fix this?

Reason
This issue occurs when a component is re-rendered when visiting other routes.
Solution
When I put the window.Echo on child component, the Echo listens many times based on the number of visits. That's why, I put it at the uppermost / parent component so that it won't re-render the component.
As much as possible, I put it at the _app.js level.

Related

Is there anyway to ignore the *failure: Avoided redundant navigation to current location* error using Vue Router and just refresh the page?

I see this question has been asked a few times on here, but none of the answers have really helped me in this current situation.
I have an app I'm working on with a sidebar with tabs that link to different dashboards. Each of the SidebarLinks are a router-link with the to key being fed the route prop from the main component.
Inside one of these dashboards, the Analysis dashboard, there is another router that routes you to child routes for specific Analyses with their own ids (EX: /analysis/1).
The user clicks on a button for a specific analysis and they are routed to a page containing that information, on the same page.
The Error
When I click the Analysis SidebarLink the route in the url changes back to /analysis, but the page doesn't update/refresh.
I don't get an error in the console, but I do get the failure in the devtools.
I understand that Vue Router doesn't route back to a route you are already on, but I need it to. If you refresh the page when the url is just /analysis it routes back to it's inital state.
Is there anyway to refresh when it rereoutes to /analysis? Or a way to handle this error to work as intended?
What I've tried
I've tried changing the router-link to an <a> tag and programatically use router.push and then catch the error, but that doesn't do anything.
I've tried checking if the route.fullPath.contains("/analysis") and then just do router.back() but that doesn't seem to work either.
SidebarLink router function
function goToRoute() {
console.log(`route.fullPath → `, route.fullPath)
if (route.fullPath.match('/analysis*') as any) {
console.log('route includes /analysis')
router.back()
} else {
console.log('route doesnt inclue /analysis')
router
.push({
path: props.route,
})
.catch(() => {})
}
}
Inital /analysis Page
This is what the page looks like normally
/analysis/1 Page
This is what the route to analysis/1 looks like (url changes)
/analysis/1 Page When Issue Analysis SidebarLink Clicked
This is what the route to analysis looks like when the sidebarlink is clicked (url changes, but the page stays the same)
I suspect you are fetching your data from a backend service or data files
If yes you can refetch the data everytime the route param changed by watching it.
watch: {
'$route.params.id': function (id) {
if(id)
this.$store.dispatch('fetchOneAnalys', id)
else
this.$store.dispatch('fetchAllAnalyses')
}

Vue PWA caching routes in advance

I'm hoping someone can tell me if I'm barking up the wrong tree. I have built a basic web app using Vue CLI and included the PWA support. Everything seems to work fine, I get the install prompt etc.
What I want to do, is cache various pages (routes) that user hasn't visited before, but so that they can when offline.
The reason here is that I'm planning to build an app for an airline and part of that app will act as an in flight magazine, allowing users to read various articles, however the aircrafts do not have wifi so the users need to download the app in the boarding area and my goal is to then pre cache say the top 10 articles so they can read them during the flight.
Is this possible? and is PWA caching the right way to go about it? Has anyone does this sort of thing before?
Thanks in advance
To "convert" your website to an PWA, you just need few steps.
You need to know that the service worker is not running on the main thread and you cant access for example the DOM inside him.
First create an serviceworker.
For example, go to your root directory of your project and add a javascript file called serviceworker.js this will be your service worker.
Register the service worker.
To register the service worker, you will need to check if its even possible in this browser, and then register him:
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/serviceworker.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope');
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
In vue.js you can put this inside mounted() or created() hook.
If you would run this code it will say that the service worker is successfully registered even if we havent wrote any code inside serviceworker.js
The fetch handler
Inside of serviceworker.js its good to create a variable for example CACHE_NAME. This will be the name of your cache where the cached content will be saved at.
var CACHE_NAME = "mycache_v1";
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open(CACHE_NAME).then(function(cache) {
return cache.match(event.request).then(function (response) {
return response || fetch(event.request).then(function(response) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
Everytime you make a network request your request runs through the service worker fetch handler here first. You need to response with event.respondWith()
Next step is you first open your cache called mycache_v1 and take a look inside if there is a match with your request.
Remember: cache.match() wont get rejected if there is no match, it just returns undefined because of that there is a || operator at the return statement.
If there is a match available return the match out of the cache, if not then fetch() the event request.
In the fetch() you save the response inside the cache AND return the response to the user.
This is called cache-first approach because you first take a look inside the cache and in case there is no match you make a fallback to the network.
Actually you could go a step further by adding a catch() at your fetch like this:
return response || fetch(event.request).then(function(response) {
cache.put(event.request, response.clone());
return response;
})
.catch(err => {
return fetch("/offline.html")
});
In case there is nothing inside the cache AND you also have no network error you could response with a offline page.
You ask yourself maybe: "Ok, no cache available and no internet, how is the user supposed to see the offline page, it requires internet connection too to see it right?"
In case of that you can pre-cache some pages.
First you create a array with routes that you want to cache:
var PRE_CACHE = ["/offline.html"];
In our case its just the offline.html page. You are able to add css and js files aswell.
Now you need the install handler:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(PRE_CACHE);
})
);
});
The install is just called 1x whenever a service worker gets registered.
This just means: Open your cache, add the routes inside the cache. Now if you register you SW your offline.html is pre-cached.
I suggest to read the "Web fundamentals" from the google guys: https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook
There are other strategies like: network-first
To be honest i dont know exactly how the routing works with SPAs because SPA is just 1 index.html file that is shipped to the client and the routing is handled by javascript you will need to check it out witch is the best strategie for your app.

How do I detect the page the user came from in vue.js when the page opens in a new tab?

I've got an interesting problem in my vue.js application and I don't know how to solve it.
We've got a "my listings" page that shows a grid of listings that the user created. When they click on one, it takes them to the listing details page. It opens this page in a new browser tab.
What we want to do is add a new component to the top of the page that shows the user the stats on their listing. But we want this component to show up ONLY when they come to the listing details page from the My Listings page. There are other ways of getting to the Listing Details page and we don't want the stats component to show up when they come from these other ways.
I would think this could be handled in the router. I tried seeing if I could detect that the user was coming from the My Listings page from the "from" parameter in the beforeEach(...) method of the router. I did this:
router.beforeEach(async (to, from, next) => {
console.log('from=', from);
console.log('to=', to);
});
When it prints the from parameter, I get this:
to= {
fullPath: "/"
hash: ""
matched: []
meta: {}
name: null
params: {}
path: "/"
query: {}
}
It contains no information about where it came from. I'm guessing this is because it opens the Listing Details page in a new tab. So I can't use the router to tell where the user came from.
Instead, I resorted to using localStorage:
On the My Listings page:
<v-btn :href="`/listings/${listing.listingId}`" target="_blank" #click="saveFromMyListings();">View Listing</v-btn>
...
saveFromMyListings() {
localStorage.setItem('from-my-listings', true);
},
On the Listing Details page:
async created () {
this.fromMyListings = localStorage.getItem('from-my-listings') === 'true';
localStorage.setItem('from-my-listings', false);
},
So long as I set the 'from-my-listings' item in localStorage to false immediately after I use it to determine that the user came from the My Listings page, it works. That way, it is ONLY set if the user comes from the My Listings page, and never set if the user comes from anywhere else.
The problem with this method is that if the user refreshes the page, the stats disappear. Obviously, this is because created() reruns and this time 'from-my-listings' is removed from localStorage. I can fix this by not setting it to false in created() once it's used, but then where do I remove it in such a way that it's guaranteed to be removed no matter how the user leaves the page (entering a new url directly in the browser, closing the browser, computer loses power, etc.)?
Is there some other hook in vue.js besides created() that runs only once (when the user first visits the page) but not on subsequent loads (like refresh)? Is there a way to pass props to a component in the router based on the state of localStorage that won't have to be passed again on refresh? What other solutions might there be to this problem?
You could use query parameters. You'd have to change the links to something like this:
yourapp.com/listing-detail/333?from=list
then in the created function you can check window.location.search for the from value

How to handle navigation when removing the current page's Vuex record?

I have a ClientManagePage where I display client information and allow for the removal of the displayed client.
The vue-router route configuration for that page looks like this:
{
path: '/client/:id/manage',
name: 'client',
component: ClientManagePage,
props: ({ params }) => ({ id: params.id }),
}
The client entities are stored in a vuex store. ClientManagePage gets its client entity from the store using the id prop and displays various properties of the client and a "remove" button.
The remove button listener is (inside a mapActions):
async removeClientClicked(dispatch) {
// Wait for the action to complete before navigating to the client list
// because otherwise the ClientListPage might fetch the client list before
// this client is actually deleted on the backend and display it again.
await dispatch('removeClientAction', this.id);
this.$router.push({ name: 'clientList' });
},
The vuex action that removes a client is:
async function removeClientAction({ commit }, id) {
// Remove the client from the store first (optimistic removal)
commit('removeClient', id);
// Actually remove the client on the backend
await api.remove('clients', id);
// Moving "commit('removeClient', id);" here still produces the warning mentioned below
}
My problem is how to handle navigating to the other route when removing a client. The current code produces warnings in development mode such as:
[Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"
found in
---> <ClientManagePage> at src/pages/ClientManagePage.vue
<Root>
This is of course caused by the reactivity system kicking in and trying to update the content of the page with the now-deleted vuex client entity. This happens before the removeClientAction is completed therefore the navigation to the ClientList page.
I've come up with some possible solutions to this, but they are not very appealing:
Have a v-if="client" at the top of the ClientManagePage that hides everything while the client does not exist in the store.
Use the computed property client in ClientManagePage to return a default "dummy" client that contains the required properties for the page. The page will still flash with "fake" content while the action is underway though.
Navigate to "clientList" right after (or even before) dispatching removeClientAction. This causes the clientList to display the removed client briefly while the action completes which is not good.
Are there other solutions to this seemingly common problem of navigating away when deleting the underlying vuex entity displayed on the current page?
I ended up doing a big v-if at the top of the ClientManagePage that hides everything while the client does not exist in the store. It's not pretty, but it works. An improvement could be to display a "please wait, operation in progress" in v-else.
One option is to externalize the deletion of the record. There are a number of ways to do that, but the simplest for me was to create a new route, /records/delete/:id, and place a route guard on that route that triggers the removal. Then redirect to the records list where you wanted to go in the first place. Something along the lines of:
import store from "wherever/your/store/is";
const routes = [{
path: "/records/delete/:id",
name: "deleteRecord",
props: true,
beforeEnter: (to, from, next) => {
store.dispatch("DELETE_RECORD", to.params.id).then(() => console.log("record deleted!"));
next({name: "some/path/you/wanted/to/go/to"});
}
}, ...];

Nuxt loading progress bar loads multiple times

I'm using default nuxt loading bar, it works well on simple pages. But when I use multiple axios requests progress bar loads every time request is sent. I want progress bar to understand all those request as a single page load. I used Promise.all and it kind of worked. But my problem is that I am using asynchronous vuex dispatch methods.
So my code is something like this, with three different asynchronous dispatch and progress bar loads three times. How can I make it so, that it loaded only once. Thanks
async fetch({ store }) {
await store.dispatch('LOAD_DATA_1')
await store.dispatch('LOAD_DATA_2')
await store.dispatch('LOAD_DATA_3')
}
It's loading three separate times because your requests are taking place sequentially, one after another, not all at once. To get around this, you can manually start/stop the loader.
First, you'll want to prevent the nuxt axios plugin from triggering the loading bar. See here.
this.$axios.$get('URL', { progress: false })
Then, you can manually start and stop the loading bar programatically before/after the requests are completed.
this.$nuxt.$loading.start()
this.$nuxt.$loading.stop()
Full example:
async fetch({ store }) {
this.$nuxt.$loading.start()
await store.dispatch('LOAD_DATA_1')
await store.dispatch('LOAD_DATA_2')
await store.dispatch('LOAD_DATA_3')
this.$nuxt.$loading.stop()
}
edit 1 (see comment):
To use in asyncData/fetch you can use the following. I'm not sure you should be accessing the components like this, but I don't see another way to access the $loading module within the context...
async fetch(ctx) {
// access the loading component via the access context
ctx.app.components.NuxtLoading.methods.start()
// example, wait 3 seconds before disabling loader
await new Promise(resolve => setTimeout(resolve, 3000))
ctx.app.components.NuxtLoading.methods.finish()
},