How to execute vue-router beforeEach ONE time? - vue.js

I have an annoying situation where I need to hook into every route change to check something. If condition X is true, I need to redirect to the homepage by showing a notice only on that single pageview. So I have the following:
router.beforeEach(() => {
if (...) {
store.showIneligibleBanner();
return { path: '/' };
}
// in all other cases hide the banner
store.hideIneligibleBanner();
return true;
});
The problem is when I return { path: '/' } this triggers the beforeEach a second time and the conditional no longer applies. I know I could create more variables to keep track but I wanted a cleaner approach.
I am really just trying to show a banner a single time on that return redirect.

In store's state, store two vars: hasBannerBeenShown and isBannerVisible
router:
// ...
router.beforeEach((to, from, next) => {
if (!store.hasBannerBeenShown && anyOtherFancyCondition) {
store.hasBannerBeenShown = true;
store.isBannerVisible = true;
return { path: '/' };
}
store.isBannerVisible && (store.isBannerVisible = false);
next()
});
Note: The syntax above is based on the assumption you're using pinia. If you're using vuex, the syntax changes a bit (you'll need to commit mutations rather than assign to state), but the logic stays the same.

That method takes a to (and from) parameters, which are the . So you can check if to.path === '/'.

Related

Dynamically add a route in a Nuxt3 middleware

I have a Nuxt3 project where I'd like to add new routes based on an API call to a database. For example, let's say a user navigates to /my-product-1. A route middleware will look into the database and if it finds an entry, it will return that a product page should be rendered (instead of a category page, for example).
This is what I came up with:
export default defineNuxtPlugin(() => {
const router = useRouter()
addRouteMiddleware('routing', async (to) => {
if (to.path == '/my-awesome-product') {
router.addRoute({
component: () => import('/pages/product.vue'),
name: to.path,
path: to.path
})
console.log(router.hasRoute(to.path)) // returns TRUE
}
}, { global: true })
})
To keep it simple, I excluded the API call from this example. The solution above works, but not on initial load of the route. The route is indeed added to the Vue Router (even on the first visit), however, when I go directly to that route, it shows a 404 and only if I don't reload the page on the client does it show the correct page when navigated to it for the second time.
I guess it has something to do with the router not being updated... I found the following example in a GitHub issue, however, I can't get it to work in Nuxt3 as (as far as I'm aware) it doesn't provide the next() method.
When I tried adding router.replace(to.path) below the router.addRoute line, I ended up in an infinite redirect loop.
// from https://github.com/vuejs/vue-router/issues/3660
// You need to trigger a redirect to resolve again so it includes the newly added
route:
let hasAdded = false;
router.beforeEach((to, from, next) => {
if (!hasAdded && to.path === "/route3") {
router.addRoute(
{
path: "/route3",
name: "route3",
component: () => import("#/views/Route3.vue")
}
);
hasAdded = true;
next('/route3');
return;
}
next();
});
How could I fix this issue, please?
Edit:
Based on a suggestion, I tried using navigateTo() as a replacement for the next() method from Vue Router. This, however, also doesn't work on the first navigation to the route.
let dynamicPages: { path: string, type: string }[] = []
export default defineNuxtRouteMiddleware((to, _from) => {
const router = useRouter()
router.addRoute({
path: to.path,
name: to.path,
component: () => import ('/pages/[[dynamic]]/product.vue')
})
if (!dynamicPages.some(route => route.path === to.path)) {
dynamicPages.push({
path: to.path,
type: 'product'
})
return navigateTo(to.fullPath)
}
})
I also came up with this code (which works like I wanted), however, I don't know whether it is the best solution.
export default defineNuxtPlugin(() => {
const router = useRouter()
let routes = []
router.beforeEach(async (to, _from, next) => {
const pageType = await getPageType(to.path) // api call
if (isDynamicPage(pageType)) {
router.addRoute({
path: to.path,
name: to.path,
component: () => import(`/pages/[[dynamic]]/product.vue`),
})
if (!routes.some(route => route.path === to.path)) {
routes.push({
path: to.path,
type: pageType,
})
next(to.fullPath)
return
}
}
next()
})
})
I suggest you use dynamic routing within /page directory structure - https://nuxt.com/docs/guide/directory-structure/pages#dynamic-routes
The [slug] concept is designed exactly for your usecase. You don't need to know all possible routes in advance. You just provide a placeholder and Nuxt will take care of resolving during runtime.
If you insist on resolving method called before each route change, the Nuxt's replacement for next() method you're looking for is navigateTo
https://nuxt.com/docs/api/utils/navigate-to
And I advise you to use route middleware and put your logic into /middleware/routeGuard.global.ts. It will be auto-executed upon every route resolving event. The file will contain:
export default defineNuxtRouteMiddleware((to, from) => {
// your route-resolving logic you wanna perform
if ( /* navigation should happen */ {
return navigateTo( /* your dynamic route */ )
}
// otherwise do nothing - code will flow and given to.path route will be resolved
})
EDIT: However, this would still need content inside /pages directory or some routes created via Vue Router. Because otherwise navigateTo will fail, as there would be no route to go.
Here is an example of one possible approach:
https://stackblitz.com/edit/github-8wz4sj
Based on pageType returned from API Nuxt route guard can dynamically re-route the original URL to a specific slug page.

Adding default query to all router links in Vue?

I'm trying to find a way to add default query to all router-links on a page (in all components that it has)?
For example, I want all links on a page to end with argument: utm_campaign=from_our_friends.
And this page uses components that are also used by other pages.
You can add a navigation guard to the page component that adds an extra query param to the next destination:
beforeRouteLeave(to, from, next) {
const query = { ...to.query, utm_campaign: "from_our_friends" };
next({ query });
}
Based on the answer by #Majed Badawi, I ended up applying this solution:
beforeRouteLeave(to, from, next) {
const query = {
...to.query,
utm_campaign: 'from_our_friends'
};
if (!to.query.utm_campaign) { // otherwise you'll get 'infinite redirection' error
next({ path: to.path, query });
} else {
next()
}
}

How to execute code after another part is completed EXPO REACT NATIVE

I'm trying to save record into database, if this record is not in user's profile (he did not discover this place) but also this record exists in collection of all places.
I'm using Expo react native and I think my problem is, that if condition will execute before functions recordInUsersAccount and recordInGlobalDatabase . Is there any way how to ensure execution after these two functions are copleted? In functions I'm rewriting variables in state={}, so I can check them below. (I tried .then() and await, async but I was not succesful).
Thank you very much.
saveScannedQrCode(idOfPlace) {
this.recordInUsersAccount(idOfPlace);
this.recordInGlobalDatabase(idOfPlace);
if (!this.state.placeAlreadyScanned && this.state.placeExistInDatabase) {
// we will add this record into database
} else {
// we will NOT add this record into database
}
}
This is the code of functions:
recordInUsersAccount(idOfPlace) {
const userId = auth.currentUser.uid;
const usersRef = db
.collection("placesExploredByUsers") // default
.doc("mUJYkbcbK6OPrlNuEPzK") // default
.collection("s53sKFeF5FS0DjuI2cdO1Rp9sCS2") // uid
.doc(idOfPlace); // id of place
usersRef.get().then((docSnapshot) => {
if (docSnapshot.exists) {
this.setState({
placeAlreadyScanned: true, // place is in user's database
});
} else {
this.setState({
placeAlreadyScanned: false, // place is NOT in user's database
});
}
});
}
recordInGlobalDatabase(idOfPlace) {
const usersRef = db
.collection("databaseOfPlaces") // default
.doc(idOfPlace); // id of place
usersRef.get().then((docSnapshot) => {
if (docSnapshot.exists) {
this.setState({
placeExistInDatabase: true, // place is in global database of places
});
} else {
this.setState({
placeExistInDatabase: false, // place is NOT in global database of places
});
}
});
}
The problem with the code is that setState in React is async, and you're trying to check the values straight after executing the functions which modify the state.
Assuming that your methods work fine and do what they're supposed to do, you could do something like:
Leave your methods as they are right now, modifying the state.
Call saveScannedQRCode as your are doing now, triggering both of the helper methods.
Instead of checking the state right after calling them, you could do that in the componentDidUpdate lifecycle hook.
componentDidUpdate(prevProps, prevState) {
if (prevState.placeExistInDatabase !== this.state.placeExistInDatabase && prevState.placeAlreadyScanned !== this.state.placeAlreadyScanned) {
// do something here - at this point state is already updated and ready to use.
// you could check for the values you're waiting for to update the DB, else not do anything.
}
}
saveScannedQrCode(idOfPlace) {
this.recordInUsersAccount(idOfPlace);
this.recordInGlobalDatabase(idOfPlace);
}
One thing - be sure to reset the state (e.g. set it to null) once you've processed the update, this way your componentDidUpdate hook won't have any problems and your strict equality will be fine.

vue-router next method not working always in beforeach

i'm creating a navigation guard for redirect user to some page,
i get auth status from vuex:
state: {
auth: false,
},
and in vue-router beforeach, i set a condition that when auth state is false and route is not adminAuth,
redirect to adminAuth route
var auth = store.state.auth
if (!auth){
if( to.name !== "adminAuth" ){
next( { name: 'adminAuth' } )
}
}
problem is when route changed first time, next() not working properly, but second time, working properly!
can you help me?
You want to intercept the navigation request using beforeResolve or beforeEnter because beforeEach is too soon and some attributes of the logic you want to use have not been processed yet, so it is possible on second navigation that some underlying logic appears resolved because the next request is accessing something set by the previous request.
const router = new VueRouter({
routes: { ... }
})
router.beforeResolve((to, from, next) => {
if(!router.app.$store.state.auth && to.name !== 'adminAuth') {
next( { name: 'adminAuth' } )
}
next()
})

How to make Vue Router Guards wait for Vuex?

So all answers I've found publicly to this question weren't very helpful and while they "worked", they were incredibly hacky.
Basically I have a vuex variable, appLoading which is initially true but gets set to false once all async operations are complete. I also have another vuex variable called user which contains user information that gets dispatched from the async operation once it gets returned.
I then also have a router guard that checks;
router.beforeEach((to, from, next) => {
if (to.matched.some(route => route.meta.requiresAuth)) {
if (store.getters.getUser) {
return next();
}
return router.push({ name: 'index.signup' });
}
return next();
});
In my initial Vue instance I then display a loading state until appLoading = false;
Now this "works" but there is a problem which is really bugging me. If you load the page, you will get a "flicker" of the opposite page you are supposed to see.
So if you are logged in, on first load you will see a flicker of the signup page. If you aren't logged in, you will see a flicker of the logged in page.
This is pretty annoying and I narrowed the problem down to my auth guard.
Seems it's pushing the signup page to the router since user doesn't exist then instantly pushes to the logged in page since user gets committed.
How can I work around this in a way that isn't hacky since it's kinda annoying and it's sort of frustrating that Vue doesn't have even the slightest bit of official docs/examples for a problem as common as this, especially since such a large number of webapps use authentication.
Hopefully someone can provide some help. :)
The router beforeEach hook can be a promise and await for a Vuex action to finish. Something like:
router.beforeEach(async (to, from, next) => {
if (to.matched.some(route => route.meta.requiresAuth)) {
await store.dispatch('init');
if (store.getters.getUser) {
return next();
}
return router.push({ name: 'index.signup' });
}
return next();
});
The 'init' action should return a promise:
const actions = {
async init({commit}) {
const user = await service.getUser();
commit('setUser', user);
}
}
This approach has the problem that whenever we navigate to a given page it will trigger the 'init' action which will fetch the user from the server. We only want to fetch the user in case we don't have it, so we can update the store check if it has the user and fetch it acordingly:
const state = {
user: null
}
const actions = {
async init({commit, state}) {
if(!state.user) {
const user = await service.getUser();
commit('setUser', user);
}
}
}
As per discussion in comments:
Best approach for you case will be if you make your appLoading variable a promise. That's how you can do things or wait for things until your app data is resolved.
Considering appLoading a promise which you initialize with your api call promise, your router hook will be like:
router.beforeEach((to, from, next) => {
appLoading.then(() => {
if (to.matched.some(route => route.meta.requiresAuth)) {
if (store.getters.getUser) {
return next();
}
return router.push({ name: "index.signup" });
}
return next();
});
});
You might want to just keep it as an export from your init code instead of keeping it in Vuex. Vuex is meant for reactive data that is shared over components.