Vue router beforeEach async/await page scroll issue - vue.js

Pages are showing strange scroll behavior when clicking back/front. When triggering those events, the current page scrolls to the position of the back or front page. In other words, the current page scrolls to the targeted position first (savedPosition) and then the page changes.
I am using the scrollBehavior method in vue-router, but the issue seems to be irrelevant to that (I have deleted the scrollBehavior, yet same problem happens).
While tracking down the problem, I found where the problem might be caused by, the router navigation.
In vue-router beforeEach, I'm fetching data using async/await as below.
router.beforeEach((to, from, next) => routerHandler(to, from, next))
routerHandler code is below.
export const routerHandler = async (to, from, next) => {
if (to.path !== '/error') {
console.log('window scrollY before await : ', window.scrollY)
console.log('window location before await : ', window.location.hash)
GetData().then(response => {
console.log('window scrollY after await : ', window.scrollY)
console.log('window location after await : ', window.location.hash)
// setting data in vue store
// auth checking process
if (condition) {
next()
} else {
next(route)
}
}).catch(e => {
console.log('Error:', e)
})
return
}
next()
}
What I found out so far is that it seems while waiting for the data to be fetched, the current page scroll moves. If I remove the "await" and "return", then the issue doesn't occur. However, it is just because next() which is at the bottom is being called and the inner logic is processed after it.
The console.log result is something like the following
window scrollY before await : 0
window location before await : #/location/one
window scrollY after await : 813
window location after await : #/location/one
This log shows that before the next() is called and page transition happens, scroll changes within the same page...
So, my question is does using async/await inside vue-router beforeeach cause scroll issues in vue.js? And since I need to fetch data to create auth checking logic before each routing, what can I do to prevent the weird scroll behavior when clicking back/front. (not the button that I created, but the one the browser has... called popstate, I guess?)
I would be very grateful if anyone could help me out with this!

Related

(vue router) disabling browser back and forward arrows, with warning

I'm creating a multipage quizz, so I need the user not to be able to go back to the previous page. So I wrote:
const router = createRouter({
history: createMemoryHistory(),
routes
});
It's working (navigation is disabled), but it the user hits back a few times, it ends up leaving the page without warning.
Is there a way to add a warning for the user?
Thanks in advance
Regards
You could use a global navigation guard to check if the user is navigating to a recognised route or not, and prompt for confirmation before navigating away.
Something like:
router.beforeEach((to, from) => {
const route = this.$router.resolve(to)
if (route.resolved.matched.length) {
// the route exists, return true to proceed with the navigation
return true
}else{
//the route does not exists, prompt for confirmation
return confirm("Are you sure you want to leave this site?")
}
})
In turns out the solution is outside Vue/VueRouter:
window.addEventListener('beforeunload', function (e) {
e.preventDefault();
e.returnValue = '';
});
Now the Vue-specific navigation is not recorded by the browser, and clicking the Back arrow displays the browser's built-in message.

Next.JS Abort fetching component for route: "/login"

I was developing a useUser Hook for per-page authentication. I have implemented the useUser hook normally and Redirecting works fine accordingly.
But I am getting the above error.
Abort fetching component for route: "/login"
How can I fix useUserHook to solve it??
//useUser.tsx
const useUser = ({ redirectTo, redirectIfFound }: IParams) => {
const { data, error } = useRequest("authed", isAuthed);
const user = data?.data;
const hasUser = user;
useEffect(() => {
if (!redirectTo) return;
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !hasUser) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && hasUser)
) {
Router.push(redirectTo);
}
}, [redirectTo, redirectIfFound, hasUser]);
return error ? null : user;
};
//index.tsx
const Home: NextPage = () => {
const user = useUser({ redirectTo: "/login" });
if (user === undefined || user === false) {
return <div>Loading...</div>;
}
return (
<div>
<Head>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div>Home</div>
</div>
);
};
UseRequest Hook returns true and false as return values.
tl;dr
Ensure that you only call router.push() once throughout all potential re-executions of useEffect with the help of state:
const [calledPush, setCalledPush] = useState(false); // <- add this state
// rest of your code [...]
useEffect(() => {
if (!redirectTo) return;
if (
(redirectTo && !redirectIfFound && !hasUser) ||
(redirectIfFound && hasUser)
) {
// check if we have previously called router.push() before redirecting
if (calledPush) {
return; // no need to call router.push() again
}
Router.push(redirectTo);
setCalledPush(true); // <-- toggle 'true' after first redirect
}
}, [redirectTo, redirectIfFound, hasUser]);
return error ? null : user;
};
Background
useEffect potentially gets called multiple times if you have more than one dependency (Also happens with React Strict Mode enabled, but in this case there seems to be no error), and (re-)calling router.push() multiple times within the same Next.js page in different places/throughout different re-renders seems to cause this error in some cases, as the redundant router.push() call(s) will have to be aborted, because the current page-component unmounts due to the successful, previously called router.push().
If we keep track of whether we have already called router.push via the calledPush state as in the code snippet above, we omit all redundant router.push() calls in potential useEffect re-executions, because for all subsequent useEffect executions the state value calledPush will already be updated to true as useEffect gets triggered after re-renders, hence after setCalledPush(true) takes effect.
In my case I have use rotuer.push("/") two times in a single file. That caused the error. Try using one. I think problem will be solved.
This error occurs because useEffect tries to update a component that has already been unmounted and this can introduce memory leaks in which your app uses more memory than it needs to. To prevent this, use the following approach:
useEffect(() => {
//first manually mount the effect
let mounted = true;
//check if component is currently mounted
if(mounted && ...code){
router.push('/index')
}
//cleanup side effects before unmounting
return()=>{mounted=false}
}, [router]);
};
In my current NextJS project, when I make reactStrictMode: false,, the value to false, then it seems like the re-rendering will be gone, and component will be only rendered once.
I don't like react strict mode very much ..

Running Nuxt middleware client side after static rendering

We're switching from SPA to statically generated, and are running into a problem with middleware.
Basically, when Nuxt is statically rendered, middleware is run on the build server first, and then is run after each page navigation client side. The important point is that middleware is not run client side on first page load. This is discussed here
We work around this for some use cases by creating a plugin that uses the same code, since plugins are run on the first client load.
However, this pattern doesn't work well for this use case. The following is an example of the middleware that we want to use:
// middleware/authenticated.js
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.authenticated) {
return redirect('/login')
}
}
// Inside a component
<template>
<h1>Secret page</h1>
</template>
<script>
export default {
middleware: 'authenticated'
}
</script>
This example is taken directly from the Nuxt docs.
When rendered statically, this middleware is not called on first page load, so a user might end up hitting their dashboard before they've logged in, which causes problems.
To add this to a plugin, the only way I can think to do this is by adding a list of authenticated_routes, which the plugin could compare to and see if the user needs to be authed.
The problem with that solution though is that we'd then need to maintain a relatively complex list of authed pages, and it's made worse by having dynamic routes, which you'd need to match a regex to.
So my question is: How can we run our authenticated middleware, which is page specific, without needing to maintain some list of routes that need to be authenticated? Is there a way to actually get the middleware associated to a route inside a plugin?
To me it is not clear how to solve it the right way. We are just using the static site generation approach. We are not able to run a nuxt middleware for the moment. If we detect further issues with the following approach we have to switch.
One challenge is to login the user on hot reload for protected and unprotected routes. As well as checking the login state when the user switches the tabs. Maybe session has expired while he was on another tab.
We are using two plugins for that. Please, let me know what you think.
authRouteBeforeEnter.js
The plugin handles the initial page load for protected routes and checks if the user can access a specific route while navigating around.
import { PROTECTED_ROUTES } from "~/constants/protectedRoutes"
export default ({ app, store }) => {
app.router.beforeEach(async (to, from, next) => {
if(to.name === 'logout'){
await store.dispatch('app/shutdown', {userLogout:true})
return next('/')
}
if(PROTECTED_ROUTES.includes(to.name)){
if(document.cookie.indexOf('PHPSESSID') === -1){
await store.dispatch('app/shutdown')
}
if(!store.getters['user/isLoggedIn']){
await store.dispatch('user/isAuthenticated', {msg: 'from before enter plugin'})
console.log('user is logged 2nd try: ' + store.getters['user/isLoggedIn'])
return next()
}
else {
/**
* All fine, let him enter
*/
return next()
}
}
return next()
})
}
authRouterReady.js
This plugin ment for auto login the user on unprotected routes on initial page load dnd check if there is another authRequest required to the backend.
import { PROTECTED_ROUTES } from "~/constants/protectedRoutes";
export default function ({ app, store }) {
app.router.onReady(async (route) => {
if(PROTECTED_ROUTES.includes(route.name)){
// Let authRouterBeforeEnter.js do the job
// to avoid two isAuthorized requests to the backend
await store.dispatch('app/createVisibilityChangedEvent')
}
else {
// If this route is public do the full init process
await store.dispatch('app/init')
}
})
}
Additionally i have added an app module to the store. It does a full init process with auth request and adding a visibility changed event or just adds the event.
export default {
async init({ dispatch }) {
dispatch('user/isAuthenticated', {}, {root:true})
dispatch('createVisibilityChangedEvent')
},
async shutdown({ dispatch }, {userLogout}) {
dispatch('user/logout', {userLogout}, {root:true})
},
async createVisibilityChangedEvent({ dispatch }) {
window.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible') {
console.log('visible changed');
await dispatch('user/isAuthenticated', {}, {root:true})
}
})
},
}

Is there a way to show a bootstrap-vue $toast feedback in beforeRouteEnter?

I have a component which loads a single user, so I use a vue-router guard to load data and in case of error redirect back to the users list component.
Is there a way to show a vue-bootstrap $toast? Usually I access $toast from this component, but obviously this does not exists yet in beforeRouteEnter.
I know I could manage in other ways (show the error in the page and use created(), or use vuex to keep the error and show it in the next page), but since I am using $toast everywhere I would like to keep consistency.
I have no ideas... if only I could access the root component I would have access to $toast but I can't see a way.
// userComponent
// ...
beforeRouteEnter(to, from, next) {
Promise.all([store.dispatch("users/fetchOne", { id: to.params.id } )])
.then(next)
.catch(() => {
// this.$root.$bvToast.toast("ERROR!!!", { variant: "danger" }); // can't do this :(
next("/users");
});
},
//...
You don't have access to this inside beforeRouteEnter. So you can do below :
next(vm => {
vm.$root.$bvToast.toast(...)
})

vue-router - how to abort route change in beforeRouteEnter

I'm seeing some behaviour I don't understand in the beforeRouteEnter navigation guard with vue.js/vue-router. I understand from the docs that this guard "does NOT have access to this component instance", but that if you need to get access to the component instance you can do so by means of a callback. I've done this because I want to abort the route change if one of the props hasn't been defined (normally because of a user clicking a forward button). So this is what I have:
beforeRouteEnter(to, from, next) {
console.log("ProductDetail: routing from = "+from.path+" to "+to.path);
next(vm => {
if (!vm.product) {
console.log("Product empty: routing back one page");
vm.$router.go(-1);
}
});
},
The idea is that I test for the existence of the prop and if it's not valid, go back (or otherwise abort the route change). From the console log, I can see that what is happening, though, is that the component instance is in fact getting created, presumably as a result of the callback being called, and throwing a bunch of errors, before the vm.$router.go(-1) kicks in and takes the user back to the previous screen.
So what, if anything, can I do to actually prevent the route change from completing if one of the requisite conditions isn't present, if it's too late by the time I can test for it?
You can try this code
beforeRouteEnter(to, from, next) {
// Your code
next(vm => {
if (!vm.product) {
console.log("Product empty: routing back one page");
next(from)
}
});
}
You can read more about this guard in https://router.vuejs.org/en/advanced/navigation-guards.html
Have you tried: next(false)?
next(false): abort the current navigation. If the browser URL was changed (either manually by the user or via back button), it will be reset to that of the from route.
Reference