Vue JS (router.beforeEach) failed to convert exception to string - vue.js

I'm trying to use router.beforeEach with localStorage. If there is data in localStorage, I want to skip the homepage. If there is no data, enter the homepage. I can print the data with console.log, but the router process fails
[vue-router] uncaught error during route navigation > failed to
convert exception to string.
How do I control the navigation?
My router.js:
Vue.use(Router);
const router = new Router({
routes: [{
path: '/',
name: 'index',
components: {
default: Index,
header: MainNavbar
},
props: {
header: {
colorOnScroll: 400
}
}
},
{
path: '/landing',
name: 'landing',
components: {
default: Landing,
header: MainNavbar,
footer: MainFooter
},
props: {
header: {
colorOnScroll: 400
},
footer: {
backgroundColor: 'black'
}
}
},
{
path: '/login',
name: 'login',
components: {
default: Login,
header: MainNavbar
},
props: {
header: {
colorOnScroll: 400
}
}
},
{
path: '/profile',
name: 'profile',
components: {
default: Profile,
header: MainNavbar,
footer: MainFooter
},
props: {
header: {
colorOnScroll: 400
},
footer: {
backgroundColor: 'black'
}
}
}
],
scrollBehavior: to => {
if (to.hash) {
return {
selector: to.hash
};
} else {
return {
x: 0,
y: 0
};
}
}
});
router.beforeEach((to, from, next) => {
let adres = JSON.parse(localStorage.getItem('adres'));
if (!adres) {
next('/');
} else {
next('/login');
}
});
export default router;
Example localdata:
{
"id":1,
"adi":"Demo",
"soyadi":"Sef",
"telefon":"05322375277",
"adres":"Girne Mahallesi 6022 Sk. No:22 Kahta/Adıyaman",
"fotograf":"http://localhost:8000/media/kullanici/sef/demosef/chef-1.jpg"
}

You are creating an infinite loop where your beforeEach guard gets triggered over and over. In the beforeEach it checks whether there is an address in localStorage and redirects to either / or /login. Then again before you enter the new route beforeEach is called and checks if there is an address and redirects. The process is repeated ad infinitum. You need to call next() without any parameters somewhere in your beforeEach guard to confirm normal navigation. So you might want to do something like this ..
router.beforeEach((to, from, next) => {
if (to.path == '/') { // If we are entering the homepage.
let adres = JSON.parse(localStorage.getItem('adres'));
if (!adres) {
next();
} else {
next('/login');
}
} else { // Not entering the homepage. Proceed as normal.
next()
}
});

Related

Vuex action does not work properly in a vue mounted hook

I'm building a small e-commerce store with an admin panel for myself.
I use Firebase firestore as my backend to store all the user's data.
I have a root 'users' collection with a document for every single registered user and everything else each user has is branching out of the user doc.
Here are firestore commands i perform so you understand the structure better.
db.collection('users').doc(userId).collection('categories').doc(subCategoryId)...
db.collection('users').doc(userId).collection('subcategories').doc(subCategoryId)...
I use Vuex so every time i need to change something on my firestore (update a product category, remove a category etc.), i dispatch an appropriate action.
The first thing any of those actions does is to go ahead and dispatch another action from auth.js that gets the userId.
The problem is that if the action in question should run in a mounted() lifecycle hook, then it fails to grab the userId.
In EditCategory.vue updateCategory action works perfectly well because SubmitHandler() is triggered on click event but in Categories.vue the fetchCategories does not work and spit out an error:
[Vue warn]: Error in mounted hook (Promise/async): "FirebaseError: [code=invalid-argument]: Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: null"
Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: null
Which, as far as i understand it, basically tells me that fetchCategories() action's firestore query could not be performed because the userId was not recieved.
After two days of moving stuff around i noticed that errors only occur if i refresh the page. If i switch to other tab and back on without refreshing, then fetchCategories() from Categories.vue mounted() hook works. Placing the code in to created() hook gives the same result.
I think that there is some fundamental thing i am missing about asynchronous code and lifecycle hooks.
Categories.vue component
<template>
<div class="category-main">
<section>
<div class="section-cols" v-if="!loading">
<EditCategory
v-on:updatedCategory="updatedCategory"
v-bind:categories="categories"
v-bind:key="categories.length + updateCount"
/>
</div>
</section>
</div>
</template>
<script>
import EditCategory from '#/components/admin/EditCategory.vue'
export default {
name: 'AdminCategories',
components: {
EditCategory,
},
data: () => ({
updateCount: 0,
loading: true,
categories: [],
}),
async mounted() {
this.categories = await this.$store.dispatch('fetchCategories');// FAILS!
this.loading = false;
},
methods: {
addNewCategory(category) {
this.categories.push(category);
},
updatedCategory(category) {
const catIndex = this.categories.findIndex(c => c.id === category.id);
this.categories[catIndex].title = category.title;
this.categories[catIndex].path = category.path;
this.updateCount++;
}
}
}
</script>
category.js store file
import firebase, { firestore } from "firebase/app";
import db from '../../fb';
export default {
actions: {
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
},
export default {
state: {
test: 10,
categories: [],
subCategories: [],
currentCategory: '',
},
mutations: {
setCategories(state, payload){
state.categories = payload;
},
},
actions: {
async fetchCategories({commit, dispatch}) {
try {
const userId = await dispatch('getUserId');
const categoryArr = [];
await db.collection('users').doc(userId).collection('categories').get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
categoryArr.push({ id: doc.id, ...doc.data() })
});
})
commit('setCategories', categoryArr);
return categoryArr;
} catch (err) { throw err; }
},
async updateCategory({commit, dispatch}, {title, path, id}) {
try {
const userId = await dispatch('getUserId');
console.log('[category.js] updateCategory', userId);
await db.collection('users').doc(userId).collection('categories').doc(id).update({
title,
path
})
commit('rememberCurrentCategory', id);
return;
} catch (err) {throw err;}
}
},
}
auth.js store file
import firebase, { firestore } from "firebase/app";
import db from '../../fb';
export default {
actions: {
...async login(), async register(), async logout()
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
},
},
}
index.js store file
import Vue from 'vue'
import Vuex from 'vuex'
import auth from './auth'
import products from './products'
import info from './info'
import category from './category'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
auth, products, info, category,
}
})
EditCategory.vue
export default {
name: 'EditCategory',
data: () => ({
select: null,
title: '',
path: '',
current: null
}),
props: {
categories: {
type: Array,
required: true
}
},
methods: {
async submitHandler() {
if (this.$v.invalid){
this.$v.$touch()
return;
}
try {
const categoryData = {
id : this.current,
title: this.title,
path: this.path
};
await this.$store.dispatch('updateCategory', categoryData);// WORKS!
this.$emit('updatedCategory', categoryData);
} catch (err) { throw err; }
},
},
//takes current category id from store getter
computed: {
categoryFromState() {
return this.$store.state.currentCategory;
}
},
created() {
console.log('[EditCategory.vue'], currentCategory);
},
mounted(){
this.select = M.FormSelect.init(this.$refs.select);
M.updateTextFields();
},
destroyed() {
if (this.select && this.select.destroy) {
this.select.destroy;
}
}
}
</script>
First of all, it's just a small detail, but you don't need need to make your 'getUserId' action async, since it does not use the 'await' keyword. So can simplify this :
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
}
const userId = await dispatch('getUserId')
into this :
getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
}
const userId = dispatch('getUserId')
Coming back to your id that seems to be undefined, the problem here is that your 'mounted' event is probably triggered before the 'login' can be completed.
How to solve this case ? Actually, there are a lot of different ways to approch this. What I suggest in your case is to use a middleware (or a 'route guard'). This guard can make you are verified user before accessing some routes (and eventually restrict the access or redirect depending on the user privileges). In this way, you can make sure that your user is defined before accessing the route.
This video is 4 years old so it is not up to date with the last versions of Firebas. But I suggest The Net Ninja tutorial about Vue Route Guards with Firebase if you want to learn more about this topic.
Accepted answer actually pointed me to the correct direction.
In my case i had to make a route guard for child routes.
router.vue
import Vue from 'vue'
import Router from 'vue-router'
import firebase from 'firebase/app';
Vue.use(Router);
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Home.vue')
},
{
path: '/bouquets',
name: 'bouquets',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Bouquets.vue')
},
{
path: '/sets',
name: 'sets',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Sets.vue')
},
{
path: '/cart',
name: 'cart',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Cart.vue')
},
{
path: '/login',
name: 'login',
meta: {layout: 'empty-layout'},
component: () => import('./views/empty/Login.vue')
},
{
path: '/register',
name: 'register',
meta: {layout: 'empty-layout'},
component: () => import('./views/empty/Register.vue')
},
{
path: '/admin',
name: 'admin',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Home.vue'),
children: [
{
path: 'categories',
name: 'adminCategories',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Categories'),
},
{
path: 'subcategories',
name: 'adminSubcategories',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/SubCategories'),
},
{
path: 'products',
name: 'adminProducts',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Products'),
},
]
},
{
path: '/checkout',
name: 'checkout',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Checkout.vue')
},
{
path: '/:subcategory',
name: 'subcategory',
meta: {layout: 'main-layout'},
props: true,
params: true,
component: () => import('./views/main/Subcategory.vue')
},
]
})
router.beforeEach((to, from, next) => {
//if currentUser exists then user is logged in
const currentUser = firebase.auth().currentUser;
//does a route has auth == true
const requireAuth = to.matched.some(record => record.meta.auth);
//if auth is required but user is not authentificated than redirect to login
if (requireAuth && !currentUser) {
// next('/login?message=login');
next('login')
} else {
next();
}
})
export default router;
category.js fetchCategories() action
async fetchCategories({commit, dispatch}) {
const userId = await dispatch('getUserId')
try {
const categoryArr = [];
await db.collection('users').doc(userId).collection('categories').get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
categoryArr.push({ id: doc.id, ...doc.data() })
});
})
commit('setCategories', categoryArr);
return categoryArr;
} catch (err) { throw err; }
},

Vue 3, SocketIO - won't join the room after login and router push

I am experiencing the following issue - once the user is logged in, and onMounted event is finished, SocketIO client side should join the room. But this doesn't happen for some reason. I have to manually refresh browser in order to join the room. What am I doing wrong here?
I have the following code for the SocketIO on the client side:
import { io } from "socket.io-client";
const token = window.localStorage.getItem('TOKEN') || window.sessionStorage.getItem('TOKEN')
console.log(token);
const ioSocket = io('https://dolphin-app-e4ozn.ondigitalocean.app', {
withCredentials: true,
transportOptions: {
polling: {
extraHeaders: {
'authorization': `${token}`,
},
},
},
});
export const socket = ioSocket
The vue 3 router logic:
import { createRouter, createWebHistory } from 'vue-router'
import Landing from '#/views/Landing.vue'
import Login from '#/views/login/Login.vue'
import ResetPassword from '#/views/login/ResetPassword.vue'
import ForgotPassword from '#/views/login/ForgotPassword.vue'
const routes = [
{
path: '/login',
name: 'login',
component: Login,
meta: {
isGuest: true,
title: 'Servant | Login'
}
},
{
path: '/resetPassword',
name: 'resetPassword',
component: ResetPassword,
meta: {
isGuest: true,
title: 'Servant | Reset Password'
}
},
{
path: '/forgotPassword',
name: 'forgotPassword',
component: ForgotPassword,
meta: {
isGuest: true,
title: 'Servant | Forgot Password'
}
},
{
path: '/',
name: 'landing',
component: Landing,
meta: {
requiresAuth: true,
title: 'Servant',
role: 'waiter'
}
},
{
path: '/:pathMatch(.*)*',
component: Landing
},
]
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior() {
// always scroll to top
return { top: 0 }
},
})
router.beforeEach((to, from, next) => {
document.title = to.meta.title || "Servant"
let token = window.localStorage.getItem('TOKEN') || window.sessionStorage.getItem('TOKEN')
if(to.meta.requiresAuth && !token)
{
next({name: 'login'})
}
if (token && to.meta.isGuest )
{
next({ name: 'landing' })
}
next();
});
export default router
Login component logic:
function login() {
loading.value = true
formClass = ''
if (user.remember)
{
window.localStorage.setItem('remember', user.remember)
}
mainStore
.login(user)
.then((response) => {
loading.value = false
router.push({
name: 'landing',
})
})
.catch((err) => {
loading.value = false
errorMsg.value = err.response.data.messages.error
formClass = 'was-validated'
})
}
Once the component is mounter I have following logic:
onMounted(() => {
socket.emit("join", import.meta.env.VITE_SOCKET_ROOM, (message) => {
console.log(message);
});
})
On the SocketIO server side I have following logic:
io.use((socket, next) => {
const header = socket.handshake.headers["authorization"];
if(header !== 'null')
{
jwtVerify(header, secret).then((res) => {
if (res === true) {
const jwt = jwtDecode(header);
servantID = jwt.payload.iss;
return next();
}
return next(new Error("authentication error"));
});
}
});

Vue: Not rendering the component for the second time on routing

I am facing behavior of Vue which I don't understand. I am using routing between components.
methods: {
redirectToLogin() {
this.$router.push("/login");
},
redirectToRegister() {
this.$router.push("/register");
}
}
So when load the app, route Login component, login successfully and then log out to component with methods above. After this when I am trying to route to login again the Login component is not rendered, but the route is shown in the address line
Below you can see my routes
routes: [
{path: '/', name: 'Hello', component: Hello},
{path: "/login",name:'Login', component: Login},
{path: "/register",name:'Register', component: Register},
{path: "/user/:id",name:'User', component: User},
{path: "/reset",name:'PasswordReset', component: PasswordReset},
]
I am also using Vuex can it somehow affect such behaviour?
UPD:
When I log out I see the following error in my console
TypeError: "t._data is undefined"
VueJS 14
$destroy
destroy
_
T
x
$
ji
_update
r
get
run
Yn
he
ue
vue.runtime.esm.js:1888:12
UPD 2 : Components
This is first component loaded to the app. After logging out route leads here and none of the router links work
export default {
name: 'Hello',
data() {
return {
msg: 'Work With your projects in agile manner'
}
}
}
Login component
export default {
name: "Login",
data() {
return {
errorOccurred: false,
errorMessage: '',
credentials: {
login: '',
password: ''
},
remember: false
}
},
methods: {
submit() {
this.$store.dispatch('loginUser', this.credentials).then(() => {
this.errorMessage = this.getError;
if (this.errorMessage.length) {
this.errorOccurred = true;
} else {
this.$router.push({path: '/user/' + this.getId});
}
});
this.errorOccurred = false;
},
resetPassword() {
this.$router.push("/reset");
},
},
computed: {
loginValidation() {
return this.credentials.login.length > 0
},
passwordValidation() {
return this.credentials.password.length > 0
},
getError() {
return this.$store.getters.getErrorMsg;
},
getId() {
return this.$store.getters.getUserId;
}
},
}
User component routed from login.
import NavbarCommon from "./NavbarCommon";
export default {
name: "User",
components: {NavbarCommon},
data(){
},
methods: {
loadAvatar(){
let image = '../../assets/emptyAvatar.png';
if ('avatar' in this.getUser){
image = this.getUser.avatar;
}
return image;
}
},
computed:{
getUser() {
return this.$store.getters.getUser;
}
}
}
And two two more components.
NavbarComponent - common navbar for several components
import NavbarRight from "./NavbarRight";
export default {
name: "NavbarCommon",
components: {NavbarRight},
methods:{
routeToUser(){
this.$router.push({path: '/user/' + this.getUser});
},
routeToProject(){
this.$router.push({path: '/project/' + this.getProject});
}
},
computed:{
getUser() {
return this.$store.getters.getUserId;
},
getProject(){
//TODO:
return 'get project id'
}
}
}
And right part of Navbar with logout button
export default {
name: "NavbarRight",
methods:{
logOut(){
this.$store.dispatch('logOutUser').then(()=>{
this.$router.push('/');
});
},
}
}
So the problem is really stupid.
In User component data missed return.
After adding
data(){
return {}
},
Everything started working

Nuxt asyncData result is undefined if using global mixin head() method

I'm would like to get titles for my pages dynamically in Nuxt.js in one place.
For that I've created a plugin, which creates global mixin which requests title from server for every page. I'm using asyncData for that and put the response into storage, because SSR is important here.
To show the title on the page I'm using Nuxt head() method and store getter, but it always returns undefined.
If I place this getter on every page it works well, but I would like to define it only once in the plugin.
Is that a Nuxt bug or I'm doing something wrong?
Here's the plugin I wrote:
import Vue from 'vue'
import { mapGetters } from "vuex";
Vue.mixin({
async asyncData({ context, route, store, error }) {
const meta = await store.dispatch('pageMeta/setMetaFromServer', { path: route.path })
return {
pageMetaTitle: meta
}
},
...mapGetters('pageMeta', ['getTitle']),
head() {
return {
title: this.getTitle, // undefined
// title: this.pageMetaTitle - still undefined
};
},
})
I would like to set title in plugin correctly, now it's undefined
Update:
Kinda solved it by using getter and head() in global layout:
computed: {
...mapGetters('pageMeta', ['getTitle']),
}
head() {
return {
title: this.getTitle,
};
},
But still is there an option to use it only in the plugin?
Update 2
Here's the code of setMetaFromServer action
import SeoPagesConnector from '../../connectors/seoPages/v1/seoPagesConnector';
const routesMeta = [
{
path: '/private/kredity',
dynamic: true,
data: {
title: 'TEST 1',
}
},
{
path: '/private/kreditnye-karty',
dynamic: false,
data: {
title: 'TEST'
}
}
];
const initialState = () => ({
title: 'Юником 24',
description: '',
h1: '',
h2: '',
h3: '',
h4: '',
h5: '',
h6: '',
content: '',
meta_robots_content: '',
og_description: '',
og_image: '',
og_title: '',
url: '',
url_canonical: '',
});
export default {
state: initialState,
namespaced: true,
getters: {
getTitle: state => state.title,
getDescription: state => state.description,
getH1: state => state.h1,
},
mutations: {
SET_META_FIELDS(state, { data }) {
if (data) {
Object.entries(data).forEach(([key, value]) => {
state[key] = value;
})
}
},
},
actions: {
async setMetaFromServer(info, { path }) {
const routeMeta = routesMeta.find(route => route.path === path);
let dynamicMeta;
if (routeMeta) {
if (!routeMeta.dynamic) {
info.commit('SET_META_FIELDS', routeMeta);
} else {
try {
dynamicMeta = await new SeoPagesConnector(this.$axios).getSeoPage({ path })
info.commit('SET_META_FIELDS', dynamicMeta);
return dynamicMeta && dynamicMeta.data;
} catch (e) {
info.commit('SET_META_FIELDS', routeMeta);
return routeMeta && routeMeta.data;
}
}
} else {
info.commit('SET_META_FIELDS', { data: initialState() });
return { data: initialState() };
}
return false;
},
}
}

vue-router does not stay on same page after a page refresh

I have two routes on vue-router, /user/:uid and /itinerary/:id. The first route is shown after the user logs in and from there they select the itinerary they want to view which would bring them to the next route.
While on /itinerary/:id, if I do a page refresh/reload/f5 I see the browser address bar back on /user/:uid. In the vue-devtools, it just resets to showing /user/:uid (not a case of some router.push() or whatever). Why is this happening and how can I keep the user on /itinerary/:id even if they refresh the page?
const routes = [
{ path: '/', component: LoginPanel },
{
path: '/user/:uid',
component: ItineraryList,
beforeEnter: authGuard,
},
{
path: '/itinerary/:itinerary_id',
name: 'itineraryView',
component: ItineraryBuilder,
beforeEnter: authGuard,
},
];
function authGuard(to, from, next) {
// retrieved from localStorage
if (idToken && userEmail) {
next();
} else {
next('/');
window.alert('Please login first');
}
}
Edit: Added relevant code from ItineraryBuilder
beforeRouteLeave(to, from, next) {
if (this.confirmed) { // set on data() property
next();
} else {
this.$modal.show('dialog', {
title: 'Confirm save new changes?',
text: '',
buttons: [{
title: 'Yes',
handler: () => {
let itinerary_id = this.$route.params.itinerary_id
let queries = [];
this.itinerary.forEach(item => {
console.log(item);
let board_id = item.doc_id;
queries.push(db.collection('itineraries').doc(board_id).set(item, { merge: true }));
});
Promise.all(queries).then(() => {
// this.$store.commit('UPDATE_ITINERARY', this.itinerary);
this.$modal.hide('dialog');
this.confirmed = true
eventBus.$emit('itinerary_saved', true);
next();
})
}
},
{
title: 'No',
handler: () => {
this.$modal.hide('dialog');
next(false);
}
}]
});
}
}
I didn't notice it at first, but could it be related? I'm using this.confirm as a flag to handle if there are any changes made by the user, I'll emit a change event and set the flag to false - meaning there are unsaved changes so the current state is unconfirmed.