Vuejs - Trying to get the updated store.state data in my router - vue.js

I’m trying to get the store.state.role data in my router to determine which routes file it should use for the routes.
When i console.log(store.state) i see that the store.state.role has the correct data i need. But when i target the parameter like console.log(store.state.role) it shows the default value from my store which isn't the same as what i see when i console.log(store.state).
this is my store file
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state:
{
//User
login_state: false,
refreshUser: false,
role: '',
target: '',
currentProject: '',
},
mutations:
{
//Update user token
updateUserToken(state, payload)
{
localStorage.setItem('token', payload);
state.login_state = true;
},
//Update role
updateRole(state, payload)
{
state.role = payload;
},
//Login state switch
loginStateUpdate(state, payload)
{
state.login_state = payload;
},
//Refresh userdata
userRefresh(state)
{
state.refreshUser = !state.refreshUser
},
updateProject(state, payload)
{
state.currentProject = payload
}
},
getters:
{
},
actions:
{
},
modules:
{
}
})
this is my router file
import Vue from "vue";
import VueRouter from "vue-router";
import Tenant from "./tenant";
import Tenancy from "./tenancy";
import {tenancy} from '../resources/api.config'
import {Http} from '#/util/http'
import store from '../store'
Vue.use(VueRouter);
const host = window.location.host.toLowerCase().split(".")[0];
let routes;
if (host != tenancy.toLowerCase())
{
console.log(store.state.role) //Returns '' while it's 'admin'
routes = Tenant;
store.commit('updateTarget', 'tenant');
}
else
{
console.log(store.state.role) //Returns '' while it's 'admin'
routes = Tenancy;
store.commit('updateTarget', 'tenancy');
}
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
export default router;
Why is this happening and how can i get the data i need ?

#neavehni's answer, using Local Storage, may add some unwanted side-effects, such as if multiple people use the the same device. Some better options:
Navigation Guards
Using Navigation Guards, add all routes at once, then implement navigation guards to prevent users from navigating to routes that they don't have permission for. (See documentation on Navigation Guards). You can use router.beforeEach if it is possible to make a unified implementation for all routes, or define the beforeEnter guard on each route if each route has very custom requirements. One option to simplify the navigation guard code is to include some information in the meta object of each route. This object allows you to define and store any data you wish in association with the route, which can be checked in the beforeEach global navigation guard.
Dynamically Load Routes at Login
An alternative is to only load a few shared routes in router.js. Then, after the user logs in, add the appropriate routes using the router's addRoutes method. This method accepts an array of RouteConfig objects, so you could use the same route definitions you currently have. See documentation here.

The only solution i've found is instead of using vuex store you use the localstorage.

Related

Insert localstorage with vuex

My script I'm using axios and vuex but it was necessary to make a change from formData to Json in the script and with that it's returning from the POST/loginB2B 200 api, but it doesn't insert in the localstorage so it doesn't direct to the dashboard page.
**Auth.js**
import axios from "axios";
const state = {
user: null,
};
const getters = {
isAuthenticated: (state) => !!state.user,
StateUser: (state) => state.user,
};
async LogIn({commit}, user) {
await axios.post("loginB2B", user);
await commit("setUser", user.get("email"));
},
async LogOut({ commit }) {
let user = null;
commit("logout", user);
},
};
**Login.vue**
methods: {
...mapActions(["LogIn"]),
async submit() {
/*const User = new FormData();
User.append("email", this.form.username)
User.append("password", this.form.password)*/
try {
await this.LogIn({
"email": this.form.username,
"password": this.form.password
})
this.$router.push("/dashboard")
this.showError = false
} catch (error) {
this.showError = true
}
},
},
app.vue
name: "App",
created() {
const currentPath = this.$router.history.current.path;
if (window.localStorage.getItem("authenticated") === "false") {
this.$router.push("/login");
}
if (currentPath === "/") {
this.$router.push("/dashboard");
}
},
};
The api /loginB2B returns 200 but it doesn't create the storage to redirect to the dashboard.
I use this example, but I need to pass json instead of formData:
https://www.smashingmagazine.com/2020/10/authentication-in-vue-js/
There are a couple of problems here:
You do a window.localStorage.getItem call, but you never do a window.localStorage.setItem call anywhere that we can see, so that item is probably always empty. There also does not seem to be a good reason to use localStorage here, because you can just access your vuex store. I noticed in the link you provided that they use the vuex-persistedstate package. This does store stuff in localStorage by default under the vuex key, but you should not manually query that.
You are using the created lifecycle hook in App.vue, which usually is the main component that is mounted when you start the application. This also means that the code in this lifecycle hook is executed before you log in, or really do anything in the application. Instead use Route Navigation Guards from vue-router (https://router.vuejs.org/guide/advanced/navigation-guards.html).
Unrelated, but you are not checking the response from your axios post call, which means you are relying on this call always returning a status code that is not between 200 and 299, and that nothing and no-one will ever change the range of status codes that result in an error and which codes result in a response. It's not uncommon to widen the range of "successful" status codes and perform their own global code based on that. It's also not uncommon for these kind of endpoints to return a 200 OK status code with a response body that indicates that no login took place, to make it easier on the frontend to display something useful to the user. That may result in people logging in with invalid credentials.
Unrelated, but vuex mutations are always synchronous. You never should await them.
There's no easy way to solve your problem, so I would suggest making it robust from the get-go.
To properly solve your issue I would suggest using a global navigation guard in router.js, mark with the meta key which routes require authentication and which do not, and let the global navigation guard decide if it lets you load a new route or not. It looks like the article you linked goes a similar route. For completeness sake I will post it here as well for anyone visiting.
First of all, modify your router file under router/index.js to contain meta information about the routes you include. Load the store by importing it from the file where you define your store. We will then use the Global Navigation Guard beforeEach to check if the user may continue to that route.
We define the requiresAuth meta key for each route to check if we need to redirect someone if they are not logged in.
router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from '../store';
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard,
meta: {
requiresAuth: true
}
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
requiresAuth: false
}
}
];
// Create a router with the routes we just defined
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// This navigation guard is called everytime you go to a new route,
// including the first route you try to load
router.beforeEach((to, from, next) => {
// to is the route object that we want to go to
const requiresAuthentication = to.meta.requiresAuth;
// Figure out if we are logged in
const userIsLoggedIn = store.getters['isAuthenticated']; // (maybe auth/isAuthenticated if you are using modules)
if (
(!requiresAuthentication) ||
(requiresAuthentication && userIsLoggedIn)
) {
// We meet the requirements to go to our intended destination, so we call
// the function next without any arguments to go where we intended to go
next();
// Then we return so we do not run any other code
return;
}
// Oh dear, we did try to access a route while we did not have the required
// permissions. Let's redirect the user to the login page by calling next
// with an object like you would do with `this.$router.push(..)`.
next({ name: 'Login' });
});
export default router;
Now you can remove the created hook from App.vue. Now when you manually change the url in the address bar, or use this.$router.push(..) or this.$router.replace(..) it will check this function, and redirect you to the login page if you are not allowed to access it.

How do I commit a vuex store mutation from inside a vue-router route that imports "store"?

My goal is to commit (invoke/call) a mutation that I've defined in my Vuex store.
store/store.js
export default {
modules: {
app: {
state: {
shouldDoThing: false,
}
mutations: {
setShouldDoThing: (state, doThing) => { state.shouldDoThing = doThing },
}
}
}
}
Since I attach Vuex to my app, I can use this.$store.commit throughout the app in various components without issue.
main.js
import Store from 'store/store.js';
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const app = new Vue({
el: '#app-root',
store,
// ...etc
});
For example:
exampleComponent.vue
export default {
created() {
// This works!
this.$store.commit('setShouldDoThing', true);
},
}
Now I'd like to commit something from a vue-router Route file, in a beforeEnter method:
exampleRoute.js
import Store from 'store/store.js'
const someRoute = {
path: '/blah',
beforeEnter(to, from, next) {
Store.commit('setShouldDoThing', true);
next();
}
}
However, when I try the above, I get the error
TypeError: _state_store__WEBPACK_IMPORTED_MODULE_10__.default.commit is not a function
There's lots of examples online of successfully using vuex getters by importing. And, if I console.log() the Store import, I can see my entire store structure
modules:
app:
actions: {someAction: ƒ, …}
getters: {isStartingQuery: ƒ}
mutations: {ariaAnnounce: ƒ, …}
state: {…}
__proto__: Object
How can I import my Store and then commit a mutation from within a vue-router file?
I've been googling for a very long time, and didn't find a stackoverflow answer or a vue forums answer for this specific case or issue, so below is the solution that I tested and works in my case.
For whatever reason, I can't trigger commit. However, I can simply invoke the mutation directly, and this change is then reflected throughout other components (as in, a "different" store wasn't imported).
someRoute.js
import Store from 'store/store.js'
const someRoute = {
path: '/blah',
beforeEnter(to, from, next) {
Store.modules.app.mutations.setShouldDoThing(Store.modules.app.state, true);
next();
}
}
And later, in some component:
someComponent.vue
export default {
beforeMount() {
console.log(this.$store.state.app.shouldDoThing);
// true
}
}

How do I go back to the last named route with a given name in Vue Router

Lets say I have 5 named routes, Index, News, TaggedNews, NewsItem, TaggedNewsItem
To change between various routes I call router.push and it is working properly
When I am at the NewsItem or TaggedNewsItem I want to go back to the last non item url, how do I do that
Take an example history stack like this
Index
News
News
TaggedNews
TaggedNewsItem
TaggedNewsItem
when I lclick a button I want to go from the last TaggedNewsItem by N steps where N will take me to the last named route TaggedNews or News or Index whichever comes first
how do I do this using Vue Router
It gives the option go(N) where N is the number of steps, problem is I need to find N to see where the last Index or News or TaggedNews item is present
I don't think the vue router saves the history stack. You would need to save it yourself.
You can use vuex to keep track of the history stack and calculate the last different route path, and have it available everywhere you need.
An implementation could be something like:
history.js
const history = {
namespaced: true,
state: {
stack: [],
},
mutations: {
PUSH_STACK(state, routeName) => state.stack.push(routeName),
},
actions: {
pushStack({ commit }, routeName) => commit('PUSH_STACK', routeName),
},
getters: {
lastDiff(state) {
const reversed = state.stack.slice().reverse();
const diff = reversed.findIndex(route => route !== reversed[0]);
return -1 * diff;
}
}
}
export default { history }
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import { history } from './history';
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
history
}
})
export default { store }
Include it in your main.js file:
import Vue from 'vue';
import { store } from './store';
const app = new Vue({
el: '#app',
store
});
In your router file, you can add a global after hook to push the path name to the store like this:
import { store } from './store';
const router = // router config
router.afterEach((to) => store.dispatch('history/pushStack', to.name));
And in your components you can use it like this:
const backN = this.$store.getters['history/lastDiff'];
this.$router.go(backN);

Vue.js with Vue-Router AJAX call before application starts

I'm creating simple application with allows to login, restore password and some other actions for logged users.
I am also using Vue-Router. I want to create 3 different types of routes:
allow only for logged users (example: change password)
allow only for unlogged users (example: restore password)
allow for everyone (example: homepage)
I have two functions with are calling before all routes
router.beforeEach(Authentication.OnlyLoggedAllowed)
router.beforeEach(Authentication.OnlyNotLoggedAllowed)
I also want to login user if it's possible (AJAX call to API). To use it I have tried to add code BEFORE route.beforeEach(...)
import Vue from "vue"
import VueRouter from "vue-router"
import VueResource from "vue-resource"
...
Authentication.TryLogin.bind(Vue)();
...
router.beforeEach(Authentication.OnlyLoggedAllowed)
router.beforeEach(Authentication.OnlyNotLoggedAllowed)
new Vue({
router
}).$mount("div#application")
Is it stupid to add function TryLogin() to router.beforeEach(..)? AJAX call after every page change is not really smart.
But I have problem because this.$http in my TryLogin() function returns undefined.
Or maybe I should raw AJAX call if there's no other way.
After all I will make this call synchronous.
There is no shame in using jQuery, for example, for ajax calls.
If you have a global store (using vuex here) you can set a boolean to true after the first TryLogin, so you don't call it more than once after further route changes. Something like that:
import Vue from 'vue';
import Router from 'vue-router';
import store from './store';
Vue.use(Router);
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
component: Home,
},
{
path: '/profile',
component: Profile,
meta: {loggedIn: true}
},
{
path: '/signup',
component: Signup,
meta: {loggedOut: true},
}
],
});
router.beforeEach(async (to, from, next) => {
if (!store.state.userLoaded) {
await $.get('/api/account').then(
// Gets the user (or null) from the server, and
// set userLoaded to true in any case
user => store.commit('updateUser', user),
err => console.error(err)
}
}
// Check if the user needs to be logged in
if (to.meta.loggedIn && !store.state.user) {
return next ({path: '/login', query: {redirectTo: to.fullPath}});
// Check if the user needs to be logged out
} else if (to.meta.loggedOut && store.state.user) {
return next ({path: '/'});
}
// We can proceeed
next();
}
export default router;
The store.commit('updateUser', user) will set userLoaded to true regardless of whether the user is actually logged in.
Here is an example of the vuex store used:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
user: null,
userLoaded: false
},
mutations: {
updateUser: (state, user) => {
state.user = user;
state.userLoaded = true;
}
}
});
And the main vue file:
import router from './router';
import store from './store';
new Vue({
router,
store
}).$mount("div#application")

How to use Vue Router from Vuex state?

In my components I've been using:
this.$router.push({ name: 'home', params: { id: this.searchText }});
To change route. I've now moved a method into my Vuex actions, and of course this.$router no longer works. Nor does Vue.router. So, how do I call router methods from the Vuex state, please?
I'm assuming vuex-router-sync won't help here as you need the router instance.
Therefore although this doesn't feel ideal you could set the instance as a global within webpack, i.e.
global.router = new VueRouter({
routes
})
const app = new Vue({
router
...
now you should be able to: router.push({ name: 'home', params: { id: 1234 }}) from anywhere within your app
As an alternative if you don't like the idea of the above you could return a Promise from your action. Then if the action completes successfully I assume it calls a mutation or something and you can resolve the Promise. However if it fails and whatever condition the redirect needs is hit you reject the Promise.
This way you can move the routers redirect into a component that simply catches the rejected Promise and fires the vue-router push, i.e.
# vuex
actions: {
foo: ({ commit }, payload) =>
new Promise((resolve, reject) => {
if (payload.title) {
commit('updateTitle', payload.title)
resolve()
} else {
reject()
}
})
# component
methods: {
updateFoo () {
this.$store.dispatch('foo', {})
.then(response => { // success })
.catch(response => {
// fail
this.$router.push({ name: 'home', params: { id: 1234 }})
})
I a situation, I find myself to use .go instead of .push.
Sorry, no explanation about why, but in my case it worked. I leave this for future Googlers like me.
I believe rootState.router will be available in your actions, assuming you passed router as an option in your main Vue constructor.
As GuyC mentioned, I was also thinking you may be better off returning a promise from your action and routing after it resolves. In simple terms: dispatch(YOUR_ACTION).then(router.push()).
state: {
anyObj: {}, // Just filler
_router: null // place holder for router ref
},
mutations: {
/***
* All stores that have this mutation will run it
*
* You can call this in App mount, eg...
* mounted () {
* let vm = this
* vm.$store.commit('setRouter', vm.$router)
* }
*
setRouter (state, routerRef) {
state._router = routerRef
}
},
actions: {
/***
* You can then use the router like this
* ---
someAction ({ state }) {
if (state._router) {
state._router.push('/somewhere_else')
} else {
console.log('You forgot to set the router silly')
}
}
}
}
Update
After I published this answer I noticed that defining it the way I presented Typescript stopped detecting fields of state. I assume that's because I used any as a type. I probably could manually define the type, but it sounds like repeating yourself to me. That's way for now I ended up with a function instead of extending a class (I would be glad for letting me know some other solution if someone knows it).
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default (router: VueRouter) => {
return new Store({
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
import Vue from 'vue'
import App from './App.vue'
import createStore from './store'
// ...
new Vue({
router,
store: createStore(router),
render: createElement => createElement(App)
}).$mount('#app')
Initial answer content
I personally just made a wrapper for a typical Store class.
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default class extends Store<any> {
constructor (router: VueRouter) {
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
}
If You need $route You can just use router.currentRoute. Just remember You rather need router reactive if You want Your getters with router.currentRoute to work as expected.
And in "main.ts" (or ".js") I just use it with new Store(router).
import Vue from 'vue'
import App from './App.vue'
import Store from './store'
// ...
new Vue({
router,
store: new Store(router),
render: createElement => createElement(App)
}).$mount('#app')