Vue.js (v3) use error component without changing URL - vue.js

I've been searching and tinkering for a while now without any luck. I'm looking to be able to catch errors and show an "Oops" page. Just to be clear, this isn't about 404 pages (which work fine).
I've reduced this for simplicity but in the below, before a page loads, it "attempts something which may fail". When that fails, I navigate to /error which shows an error page:
const router = createRouter({
routes: [
{
path: '/',
component: Index
},
{
path: '/something',
component: Something
},
{
path: '/error',
component: Error
},
{
path: '/:catchAll(.*)',
component: NotFound
}
]
})
router.beforeEach(async (to, from, next) => {
// attempt something which may fail
})
router.onError(() => router.push('/error'))
This all works fine, but it means that the /error path is navigable, and that a path change occurs. What I'd prefer is a way to be able to show an error component (Error) if an error occurs while keeping the url path the same.
Say I was on / and then I navigated to /something but "something failed", the url path would equal /something but the Error component would be used, rather than the Something component.
Any ideas? It seems like this should be manageable but so far I'm coming up blank.
Thanks

As Duannx commented above, a solution to this was to use a state to control this. Here's how I did it:
I created a Pinia store to hold an error variable:
import { defineStore } from 'pinia'
export const useGlobalStore = defineStore({
id: 'global',
state: () => ({
_error: false
}),
getters: {
error: (state) => state._error
},
actions: {
async setError(value) {
this._error = !!value
}
}
})
In the router, I catch any errors:
import { createRouter, createWebHistory } from 'vue-router'
import { useGlobalStore } from '../stores/global'
import routes from './routes'
const history = createWebHistory(import.meta.env.BASE_URL)
const router = createRouter({ history, routes })
router.beforeEach(async (to, from, next) => {
const globalStore = useGlobalStore()
try {
// Things which may fail here
next()
} catch (error) {
globalStore.setError(true)
return next()
}
})
export default router
In the root component (App.vue), I then check for the error state, and either show that or the router view based on whether or not the error state is set the true.
<template>
<Error v-if="globalStore.error" />
<RouterView v-else />
</template>
This works great, thanks Duannx.

Related

vue-router Navigation Guard does not cancle navigation

Before accessing any page, except login and register; I want to authenticate the user with Navigation Guards.
Following you can see my code for the vue-router. The "here" gets logged, but the navigation is not cancelled in the line afterwards. It is still possible that if the user is not authenticated that he can access the /me-route
my router-file:
import { createRouter, createWebHistory } from "vue-router";
import axios from "axios";
import HomeView from "../views/HomeView.vue";
import RegisterView from "../views/RegisterView.vue";
import LoginView from "../views/LoginView.vue";
import MeHomeView from "../views/MeHomeView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: HomeView,
},
{
path: "/register",
name: "register",
component: RegisterView,
},
{
path: "/login",
name: "login",
component: LoginView,
},
{
path: "/me",
name: "me",
component: MeHomeView,
},
],
});
router.beforeEach((to, from) => {
if(to.name !== 'login' && to.name !== 'register') {
console.log(to.name);
axios.post("http://localhost:4000/authenticate/", {accessToken: localStorage.getItem("accessToken")})
.then(message => {
console.log(message.data.code);
if(message.data.code === 200) {
} else {
console.log("here");
return false;
}
})
.catch(error => {
console.log(error);
return false;
})
}
})
export default router;
Navigation guards support promises in Vue Router 4. The problem is that promise chain is broken, and return false doesn't affect anything. As a rule of thumb, each promise needs to be chained.
It should be:
return axios.post(...)
The same function can be written in more readable and less error-prone way with async..await.

Vue Router works fine in development, but doesn't match routes in production

I have a Vue SPA that's being served by an ASP Core API. When I run it in development mode, everything works perfectly. But as soon as I deploy it to production (on an Azure App Service), I always get a blank page.
It seems to be specifically the router that can't match the routes, as I can put some arbitrary HTML into my App.vue, and that will render.
If I go into the developer tools, I can see that the index.html and all .js files download successfully and there are no errors in the console. This is true no matter what URL I visit e.g. myapp.com and myapp.com/login, both download everything but nothing displays on screen.
I have seen several posts saying to change the routing mode to hash, but I still get the same result with that.
Please see below my files:
main.ts
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import vuetify from './plugins/vuetify';
import { LOGIN_INITIALISE } from './use-cases/user-auth/AuthModule';
Vue.config.productionTip = false;
store.dispatch(LOGIN_INITIALISE)
.then(() => {
new Vue({
router,
store,
vuetify,
render: (h) => h(App),
}).$mount('#app');
});
App.vue
<template>
<div>
<div>test</div>
<router-view></router-view>
</div>
</template>
<script lang="ts">
/* eslint-disable no-underscore-dangle */
import Vue from 'vue';
import Axios from 'axios';
import { LOGOUT } from './use-cases/user-auth/AuthModule';
import { LOGIN } from './router/route-names';
export default Vue.extend({
name: 'App',
created() {
// configure axios
Axios.defaults.baseURL = '/api';
Axios.interceptors.response.use(undefined, (err) => {
// log user out if token has expired
if (err.response.status === 401 && err.config && !err.config.__isRetryRequest) {
this.$store.dispatch(LOGOUT);
this.$router.push({ name: LOGIN });
}
throw err;
});
},
});
</script>
router/index.ts
import Vue from 'vue';
import {} from 'vuex';
import VueRouter, { RouteConfig } from 'vue-router';
import store from '#/store';
import {
HOME,
LOGIN,
SIGNUP,
USERS,
} from './route-names';
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
{
path: '/',
name: HOME,
component: () => import('#/views/Home.vue'),
},
{
path: '/login',
name: LOGIN,
component: () => import('#/views/Login.vue'),
},
{
path: '/signup',
name: SIGNUP,
component: () => import('#/views/SignUp.vue'),
},
{
path: '/users',
name: USERS,
component: () => import('#/views/Users.vue'),
beforeEnter: (to, from, next) => {
if (store.getters.userRole === 'Admin') {
next();
} else {
next({ name: HOME });
}
},
},
{
path: '*',
name: '404',
component: {
template: '<span>404 Not Found</span>',
},
},
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
router.beforeEach((to, from, next) => {
if (store.getters.isAuthenticated) {
next();
} else if (to.name === LOGIN || to.name === SIGNUP) {
next();
} else {
next({ name: LOGIN });
}
});
export default router;
Finally after completely rebuilding my router piece by piece, I found the issue. I found that the problem was in this global route guard:
router.beforeEach((to, from, next) => {
if (store.getters.isAuthenticated) {
next();
} else if (to.name === LOGIN || to.name === SIGNUP) {
next();
} else {
next({ name: LOGIN });
}
});
Specifically, the isAuthenticated getter was throwing an error (silently), so all of the routes were failing before they could render. I wrapped my isAuthenticated logic in a try-catch that returns false if an error is thrown, and now everything works fine.
I still don't understand why this only affects the production build, but hopefully this experience will be useful to others stuck in the same situation.

Why does my Vue Router throw a Maximum call stack error?

I have a really simple routing practically looks like this I'm using this under electron
import Vue from "vue";
import VueRouter from "vue-router";
import Projects from "../views/Projects.vue";
import RegisterUser from "#/views/RegisterUser.vue";
//import { appHasOwner } from "#/services/";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "projects",
component: Projects,
meta: {
requiresUser: true
}
},
{
path: "/register",
name: "register",
component: RegisterUser
},
{
path: "/settings",
name: "settings",
meta: {
requiresUser: true
},
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/Settings.vue")
}
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
router.beforeEach((to, from, next) => {
if (to.matched.some(route => route.meta.requiresUser === true)) {
//this will be for test case undefined
let user;
if (typeof user === "undefined") {
// eslint-disable-next-line no-console
console.log(user); //logs undefined but at the end no redirect
next("/register");
} else {
next();
}
}
});
export default router;
taking the example from the docs
// GOOD
router.beforeEach((to, from, next) => {
if (!isAuthenticated) next('/login')
else next()
})
the application can boot only if there is a user attached in database either should redirect to the register component but the code above will end with Maximum call stack size exceeded. So how to check with beforeEach conditions end redirect to a given page?
The Maximum call stack size exceeded is usually due to infinite recursion, and that certainly seems to be the case here. In router.beforeEach you're calling next to go to the /register route, which goes back into this method, which calls next, and so on. I see you have a requiresUser in your meta, so you need to check that in beforeEach, like this:
router.beforeEach((to, from, next) => {
// If the route's meta.requiresUser is true, make sure we have a user, otherwise redirect to /register
if (to.matched.some(route => route.meta.requiresUser === true)) {
if (typeof user == "undefined") {
next({ path: '/register' })
} else {
next()
}
}
// Route doesn't require a user, so go ahead
next()
}

Vue Router, refresh shows blank page

Im trying to figure out why Vue.js routing, after I refresh my admin page re-directs back to the home component, only showing a blank page, and after a second refresh shows the home component again. I am still logged in as I can still go directly with the url to my admin-page. Meaning the session is still active. Is there a way to force the page to stay on the admin home page when I press F5? I tried things like history mode etc, but cant figure it out.
This is my router.js layout in the root and alos my main router file
import Vue from 'vue'
import Router from 'vue-router'
import firebase from "firebase/app";
import Home from './views/user/Home.vue'
import adminRoute from "./components/routes/admin-routes";
Vue.use(Router);
const router = new Router({
routes: [
{
path: '*',
redirect: '/',
},
{
path: '/',
name: 'home',
component: Home,
meta: {
requiresAuth: false,
}
},
...adminRoute
],
});
router.beforeEach((to, from, next) => {
if (to.matched.some(r => r.meta.requiresAuth === false)) {
next()
} else if (!firebase.auth().currentUser) {
next(false)
} else {
next()
}
});
export default router
and I have my admin-routes.js
import AdminHome from "../../views/admin/AdminHome";
import Users from "../../views/admin/Users";
const adminRoute = [
{
path: '/admin-home',
name: 'AdminHome',
component: AdminHome,
meta: {
requiresAuth: true
},
},
{
path: '/users',
name: 'Users',
component: Users,
meta: {
requiresAuth: true,
}
}
];
export default adminRoute;
I do want to mention that my main page is under views/user/Home.vue and my AdminHome page is views/admin/AdminHome.vue
This problem is return to the server not the built app
you have to manage your 404 not found root
Here you can find the solution
and if you work With Netlify you just create a file in the root of the project ( same place as package.json) name netlify.toml containing
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
in vue4 they did away with the 'next' argument(see here), this is working for me to send people back to the login page, where I'm using firebaseui, I have a simple boolean flag in my vuex store named "isAuthenticated" that I flip on when a user signs on.
NOTE: the following is a boot file for quasar v2 but the logic will
work where ever you have access to the store
import { computed } from 'vue'
export default ({ app, router, store }) => {
console.log('nav.js:')
const isAuthenticated = computed(() => store.state.auth.isAuthenticated)
console.log('isAuthenticated',isAuthenticated.value)
router.beforeEach((to, from) => {
console.log('nav.js:beforeEach:to',to)
console.log('nav.js:beforeEach:from',from)
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isAuthenticated.value) {
console.log('nav.js:beforeEach:to.redirect')
return '/login'
}
}
})
}

How to setup vuex and vue-router to redirect when a store value is not set?

I'm working with the latest versions of vue-router, vuex and feathers-vuex and I have a problem with my router.
What I'm doing is to check if a route has the property "requiresAuth": true in the meta.json file. If it does then check the value of store.state.auth.user provided by feathers-vuex, if this value is not set then redirect to login.
This works fine except when I'm logged in and if I reload my protected page called /private then it gets redirected to login so it seems that the value of store.state.auth.user is not ready inside router.beforeEach.
So how can I set up my router in order to get the value of the store at the right moment?
My files are as follow:
index.js
import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'
const meta = require('./meta.json')
// Route helper function for lazy loading
function route (path, view) {
return {
path: path,
meta: meta[path],
component: () => import(`../components/${view}`)
}
}
Vue.use(Router)
export function createRouter () {
const router = new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: [
route('/login', 'Login')
route('/private', 'Private'),
{ path: '*', redirect: '/' }
]
})
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
if (!store.state.auth.user) {
next('/login')
} else {
next()
}
} else {
next()
}
})
return router
}
meta.json
{
"/private": {
"requiresAuth": true
}
}
I fixed the issue by returning a promise from vuex action and then run the validations
router.beforeEach((to, from, next) => {
store.dispatch('auth/authenticate').then(response => {
next()
}).catch(error => {
if (!error.message.includes('Could not find stored JWT')) {
console.log('Authentication error', error)
}
(to.meta.requiresAuth) ? next('/inicio-sesion') : next()
})
})