vuex persisted state not working with vue router navigation guard - vue.js

I've added vuex-persistedstate as defined in documentation. (confirmed and working)
export default new Vuex.Store({
modules: {
auth,
},
plugins: [VuexPersistedState()]
});
I've set up a router navigation guard to redirect to home page on login
/* Auth based route handler */
router.beforeEach((to, from, next) => {
if (to.meta.hasOwnProperty('requiresAuth')) {
if (to.meta.requiresAuth === true) {
let isAuthenticated = authStore.getters.isAuthenticated
if (isAuthenticated(authStore.state) === true) {
next()
} else {
next({name: 'login'})
}
} else {
let isAuthenticated = authStore.getters.isAuthenticated
console.log(authStore.state)
if (isAuthenticated(authStore.state) === true) {
next({name: 'home'})
} else {
next()
}
}
} else {
next()
}
})
The vuex persistedstate restores store from local storage but not before navigation guard!
I can provide any necessary part of the source code for evaluation. Please comment your request as needed. Experimental solutions are also welcome. This is just a personal training application for me!
Help is appreciated!

I know this is probably of no use to #Vaishnav, but as I wen't down the same rabbit hole recently I figured I'd post a workaround I found for this here as this was the only post I found that asked this issue specifically.
in your store/index.js You need to export both the function and the store object.
Change this :
export default() => {
return new vuex.Store({
namespaced: true,
strict: debug,
modules: {
someMod
},
plugins: [createPersistedState(persistedStateConfig)]
});
};
to:
const storeObj = new vuex.Store({
namespaced: true,
strict: debug,
modules: {
someMod
},
plugins: [createPersistedState(persistedStateConfig)]
});
const store = () => {
return storeObj;
};
export {store, storeObj}
Then also, as you have now changed the way you export the store you will also need to change the way it's imported.
Everywhere in your app you've imported the store ie: in main.js -> import store from '#/store' excluding the router.js you will need to change to import {store} from '#/store'
And in your router.js just import {storeObj} from '#/store' and use that instead of store in your router guard ie: storeObj.getters['someMod/someValue']

I found a working example.
As the router isn't a component.
In router config file -
import authStore from '#/stores/auth/store'
Instead -
import store from '#/store'
in navigation guard, I replaced
let isAuthenticated = authStore.getters.isAuthenticated
if(isAuthenticated(authstore.state) === true){...}
with -
let isAuthenticated = store.getters['auth/isAuthenticated']
if(isAuthenticated === true){...}
And now this works like charm...

Related

Vue accessing Vuex store in router file

I am trying to access my store in the router file as follow, but it's not working. Giving me undefined when I console. Any advice? I am using Vue 3. Thank you in advance!
import store from '../store/index'
const preventRoutes = {
beforeEach: (to, from, next) => {
console.log(store.getters.getLoginState);
if (store.getters.getLoginState === "true") {
console.log("i reached if")
next();
} else {
console.log("i reached else")
next("/");
}
}
}
maybe is because the before route load before the imports
try to import it directly from the before route:
router.beforeEach(function (...) {
const store = require('./store')
store.blah...
})
I have similar situation.
import store from "../store"; to router
then I can use store's API (commit etc ) inside router.beforeEach

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 to attach axios / axios interceptor to Nuxt globally ?

how would i go about attaching axios / axios interceptor globally to nuxt (so its available everywhere), same how i18n is attached ?
The idea is that i would like to have a global axios interceptor that every single request goes through that interceptor.
Thanks
you can create a plugin called axios (/plugins/axios.js)
import Vue from 'vue';
import axios from 'axios';
axios.interceptors.request.use((config) => {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
Vue.use(axios);
then define this in nuxt.config.js
module.exports = {
//....
plugins: [
'~/plugins/axios',
],
//....
};
thats all, your interceptor is now working globally
It's hidden in the documentation - https://nuxtjs.org/docs/2.x/directory-structure/plugins
See number 3 of the first photo:
// plugins/axios.js
export default function ({ $axios, redirect }) {
$axios.onError(error => {
if (error.response.status == 404) {
redirect('/sorry')
}
})
}
then define this in nuxt.config.js
module.exports = {
//....
plugins: [
'~/plugins/axios',
],
//....
};
Maybe will be helpful for someone.
It just sets the lang parameter for every request.
Сreate a plugin called axios (/plugins/axios.js). Put it there:
export default function ({ $axios, app, redirect }) {
$axios.onRequest(config => {
config.params = config.params || {}; // get existing parameters
config.params['lang'] = app.i18n.locale;
})
$axios.onError(error => {
const code = parseInt(error.response && error.response.status)
if (code === 400) {
redirect('/400')
}
})
}
Add in nuxt.config.js:
module.exports = {
plugins: [
'~/plugins/axios'
]
};
Create a new module, call it request.js for example.
import axios from 'axios'
const instance = axios.create({
baseURL: 'http://example.org' // if you have one
})
// Put all interceptors on this instance
instance.interceptors.response.use(r => r)
export default instance
Then simply import that instance whenever you need it and use it like it was a normal axios instance:
import request from './request'
await request.get('/endpoint')
// or use promises
request.get('/endpoint').then(data => data)
If you really need it globally you can use the following code in your entry point of the application:
import request from './request'
global.request = request
// use it:
await request.get('example.org')
Or you can add it to the vue protype
Vue.prototype.$request = request
// in your component:
this.$request.get()
I'd advice against it though.

Axios interceptor in vue 2 JS using vuex

I store token after success login call in vuex store like this:
axios.post('/api/auth/doLogin.php', params, axiosConfig)
.then(res => {
console.log(res.data); // token
this.$store.commit('login', res.data);
})
axiosConfig is file where I only set baseURL export default { baseURL: 'http://localhost/obiezaca/v2' } and params is just data sent to backend.
My vuex file looks is:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
logged: false,
token: ''
},
mutations: {
login: (state, response) => {
state.logged = true;
state.token = response;
console.log('state updated');
console.log('state.logged flag is: '+state.logged);
console.log('state.token: '+state.token);
},
logout: (state) => {
state.logged = false;
state.token = '';
}
}
});
It is working correctly, I can re-render some of content in my SPA basing on v-if="this.$store.state.logged" for logged user. I'm able to access this.$store.state.logged from any component in my entire app.
Now I want to add my token to every request which call my rest API backend. I've created basic axios http interceptor which looks like this:
import axios from 'axios';
axios.interceptors.request.use(function(config) {
const token = this.$store.state.token;
if(token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, function(err) {
return Promise.reject(err);
});
Now I have 2 problems/questions about it.
I know that it is available to use this.$store.state.logged or this.$store.state.token across every component but can I use it same way in single javascript file?
Where should I execute/start my interceptor javascript file? It is independent file which lays in my app main folder but I am not calling it anywhere, in angularJS which I was working before, I had to add $httpProvider.interceptors.push('authInterceptorService'); in config but I don't know how to do same thing in vue architecture. So where should I inject my interceptor?
EDIT
I followed GMaiolo tips I added
import interceptor from './helpers/httpInterceptor.js';
interceptor();
to my main.js file and I refactor my interceptor to this:
import axios from 'axios';
import store from '../store/store';
export default function execute() {
axios.interceptors.request.use(function(config) {
const token = this.$store.state.token;
if(token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, function(err) {
return Promise.reject(err);
});
}
Result of this changes is that every already existing backend calls ( GET ) which don't need token to work stopped working but it is logical because I didn't clarified to which request it should add token so it is trying to add it everywhere and in my interceptor something is still wrong and that is why every already exisitng request stopped working.
When I try to do backend POST call in browser console I still get this error:
TypeError: Cannot read property '$store' of undefined
Although I import store to my interceptor file. Any ideas? I can provide some more information if any needed.
I additionally add screenshot of this main, store and interceptor tree structure so you can see that I'm importing fron correct path:
1.
First of all I'd use a Vuex Module as this Login/Session behavior seems to be ideal for a Session module. After that (which is totally optional) you can set up a Getter to avoid accessing the state itself from outside Vuex, you'd would end up with something like this:
state: {
// bear in mind i'm not using a module here for the sake of simplicity
session: {
logged: false,
token: ''
}
},
getters: {
// could use only this getter and use it for both token and logged
session: state => state.session,
// or could have both getters separated
logged: state => state.session.logged,
token: state => state.session.token
},
mutations: {
...
}
With those getters set, you can get the values a bit easier from components. With either using this.$store.getters.logged (or the one you'd want to use) or using the mapGetters helper from Vuex [for more info about this you can check the getters docs]:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
...mapGetters([
'logged',
'token'
])
}
}
2.
I like to run Axios' interceptors along with Vue instantation in main.js creating, importing and executing an interceptors.js helper. I'd leave an example so you get an idea, but, then again, this is my own preference:
main.js
import Vue from 'vue';
import store from 'Src/store';
import router from 'Src/router';
import App from 'Src/App';
// importing the helper
import interceptorsSetup from 'Src/helpers/interceptors'
// and running it somewhere here
interceptorsSetup()
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
});
interceptors.js
import axios from 'axios';
import store from 'your/store/path/store'
export default function setup() {
axios.interceptors.request.use(function(config) {
const token = store.getters.token;
if(token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, function(err) {
return Promise.reject(err);
});
}
And there you'd end up having all the behavior cleanly encapsulated.
I did the same logic. however, I just change the file name. I used axios/index.js but the store is undefined there. so I just change the file name axios/interceptor.js and Don't know store data is accessible look at my below image

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')