I'm trying to fix a behavior in my VueJS SPA wherein a limbo state arises. The app doesn't know the JWT has already expired and therefore presents itself as if the user is still logged in. This can happen after hibernation, for example.
These users can keep on making any request to the API, but end up with a 401 response (and correctly so).
I'd like to have a global handler for 401 responses. (This would be: "clear everything user-related from vuex and present the page as if the user was a guest, with login form popup, etc.") Otherwise, I would have to write a 401 handler for EVERY request.
I can add response interceptors to axios, and they work fine. These interceptors don't have access to Vuex (or Vue), though.
Whenever I try to import Vuex or Vue into my Axios, I get circular dependencies (of course) and everything breaks.
If I just throw/return the error, I still have to handle it separately on every request. How can I dispatch methods on this.$store from within an axios interceptor?
The Axios file contains an export default class API that is added to Vue globally in main.js:
import api from 'Api/api'
// ...
Vue.prototype.$http = api
I had thought there has to be a way to access Vue from $http, since it's a global instance method. But I appear to be mistaken?
Code
main.js
// ...
import api from 'Api/api'
// ...
Vue.prototype.$http = api
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App },
vuetify: new Vuetify(opts),
});
api.js
import Client from './ApiClient'
const apiClient = new Client({ basePath: process.env.VUE_APP_API_URL })
const api = {
get(url) {
return apiClient._get(`${basePath}/${url}`)
},
post(url, data) {
return apiClient._post(`${basePath}/${url}`, data)
},
// ...
}
export default api
ApiClient.js
const axios = require('axios')
const errorHandler = (error) => {
if (error.response.status === 401) {
store.dispatch('user/logout') // here is the problem
}
return Promise.reject({ ...error })
}
export default class API {
constructor(options) {
this.options = Object.assign({ basePath: '' }, options)
this.axios = axios.create({ timeout: 60000 })
this.axios.interceptors.response.use(
response => response,
error => errorHandler(error)
)
}
// ...
}
Importing store in ApiClient.js results in a dependency cycle: I assume because I'm importing Vue in it?
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import PersistedState from 'vuex-persistedstate'
import CreateMutationsSharer from 'vuex-shared-mutations';
import SecureLS from 'secure-ls';
// import modules
Vue.use(Vuex);
const ls = new SecureLS({ encodingType: 'aes' });
export default new Vuex.Store({
// options
})
conf
import Axios from 'axios'
import IdentityProxy from './IdentityProxy.js'
import UsuariosProxi from './UsuariosProxi'
import ZonasProxi from './ZonasProxi'
//axios
Axios.defaults.headers.common.Accept='application/json'
//Axios.defaults.headers.common['Access-Control-Allow-Origin'] = '*';
Axios.interceptors.request.use(
config => {
let token = localStorage.getItem('access_token');
if(token){
config.headers= {
'x-access-token': `${token}`
}
}
return config;
},
error => Promise.reject(error)
);
Axios.interceptors.response.use(
response => response,
error => {
if (error.response.status===403||error.response.status===401) {
localStorage.removeItem('access_token');
window.location.reload(true);
}
return Promise.reject(error);
}
);
let url=null
if(localStorage.getItem("config")!==null){
let config = JSON.parse(localStorage.getItem("config"))
url = config
}
console.log(url)
export default{
identityProxy: new IdentityProxy(Axios, url),
_usuarioProxi: new UsuariosProxi(Axios, url),
_zonasProxi: new ZonasProxi(Axios, url),
}
//
export default class IdentityProxy{
constructor(axios,url){
this.axios = axios;
this.url =url;
}
register(params){
return this.axios.post(this.url+'/identity/register',params)
}
login(params){
return this.axios.post(this.url+'/auth/signin',params)
}
}
//
export default class UsuariosProxi{
constructor(axios,url){
this.axios = axios;
this.url =url;
}
/* getAll(){
return this.axios.get(this.url+'/users')
} */
getAll(page, take) {
return this.axios.get(this.url + `/users?page=${page}&take=${take}`);
}
create(params) {
return this.axios.post(this.url + '/auth/signup', params);
}
get(id) {
return this.axios.get(this.url + `/users/${id}`);
}
update(id, params) {
return this.axios.put(this.url + `/users/${id}`, params);
}
remove(id) {
return this.axios.delete(this.url + `/users/${id}`);
}
//-----APARTE SOLO TRAE LISTA DE ROLES--------
getRoles() {
return this.axios.get(this.url + '/users/newrol');
}
}
//st
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
user:null
}
export default new Vuex.Store({
state
});
main.js:
import store from './store';
const Instance = new Vue({
store,
...
})
export const { $store } = Instance;
Now you can import { $store } from '#/main.js' anywhere you want. And it's going to be the same instance you have mounted in your app, not a new Vuex.Store({}) (which is what ./store exports, each time you import it somewhere else).
You can export the same way anything else you might want to use in services, tests, helpers, etc... I.e:
export const { $store, $http, $bus, $t } = Instance;
What about direct import your store to ApiClient.js? Something like
const axios = require('axios')
import store from 'path/to/store'
const errorHandler = (error) => {
if (error.response.status === 401) {
store.dispatch('user/logout') // now store should be accessible
}
return Promise.reject({ ...error })
}
export default class API {
constructor(options) {
this.options = Object.assign({ basePath: '' }, options)
this.axios = axios.create({ timeout: 60000 })
this.axios.interceptors.response.use(
response => response,
error => errorHandler(error)
)
}
// ...
}
Base on these thread I was able to manage a solution for my needs:
main.js
import api, {apiConfig} from 'Api/api'
apiConfig({ store: $store });
ApiClient.js
let configs = {
store: undefined,
};
const apiConfig = ({ store }) => {
configs = { ...configs, store };
};
export default api;
export { apiConfig };
This way the api.js file will require a configuration that can later be expanded.
Related
I have a store.js file which sends an api request and gets responses using axios,
the api is tested and working perfectly.
store.js contains this code :
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
import cUser from './modules/User';
import UI from './modules/UI';
Vue.use(Vuex)
export default new Vuex.Store({
data: function () {
return {
responsedata: []
};
},
viewresults: async (commit, payload)=>{
let token =localStorage.getItem('token');
let username= payload.username;
await axios.post('search',{token, username}).then(response => {
this.responsdata = response.data;
}).catch(er=>{
console.log(er);
});
and i have this function in other file that uses it :
search(){
console.log('search clicked');
console.log(this.username);
this.responsedata = this.$store.dispatch('viewresults',{
username: this.username,
});
console.log(this.responsedata);
},
}
but i get this error in the browser console :
TypeError: Cannot set property 'responsedata' of undefined
it seems like that viewresult in the store.js can't see the responsedata variable defined in data return function .
Let me show you an example about how to use the Vuex store:
// main.js
import Vue from 'vue';
import App from './App.vue';
import store from '#/store/index';
new Vue({
el: '#app',
store,
components: {
App
},
render: h => h(App)
});
Now the store.js file:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios'
Vue.use(Vuex);
const state = {
responseData: []
}
const mutations = {
setResponse(state, data) {
state.responseData = data;
}
}
const actions = {
async viewResults({ commit }, payload) {
let token =localStorage.getItem('token');
let username= payload.username;
await axios.post('search', { token, username }).then(response => {
commit('setResponse', response.data);
}).catch(er=>{
console.log(er);
});
}
}
export const store = new Vuex.Store({
state,
mutations,
actions
});
And a component to call the action and show the information:
// SearchAndShowData.vue component
<template>
<div>
<button click="search">
Search
</button>
{{ responseData}}
</div>
</template>
<script>
import {mapActions, mapState} from 'vuex';
export default {
name: "SearchAndShowData",
data: () => ({
username: "Andres",
}),
computed() {
// Expose the state.responseData object as responseData
...mapState(['responseData']) // If you want to preprocess the data, use create a getter and use mapGetters
// As a computed property, it will be updated always that responseData has any change
},
methods: {
...mapActions(["viewResults"]), // Expose the viewResults action as a method, call it with this.viewResults
search() {
this.viewResults({username: this.username })
}
}
}
</script>
I didn't test it, but this is the idea behind a vuex store and how to use it, if somebody sees an error, please let me know to update the answer (thanks).
Hope you can update your code with this information and it can work properly.
Also, Scrimba has a free course that could help you to extend your knowledge about Vuex, check it here
I'm working in a vue project, I'm very new to vue.
We have a db_handler.js in out /src/utility folder.
It looks like this:
import fakeApiCall from "./mock";
import axios from "axios";
import { DEBUG_MODE, API_ENDPOINT } from "./namespaces";
function fetchData(method, slug, payload) {
//axios.defaults.headers.withCredentials = true;
//return (!DEBUG_MODE) ? axios[method](`${API_ENDPOINT}${slug}`, payload) : fakeApiCall(slug);
return axios[method](`${API_ENDPOINT}${slug}`, payload);
/*var url = "http://localhost:8080" + slug
return axios({
method: method,
url: url,
headers: {
'Authorization': payload
}
});*/
}
function sendData(method, slug, payload) {
axios[method](`${API_ENDPOINT}${slug}`, payload);
}
export default fetchData
What I need to know:
How can I export my sendData()?
They used a short syntax so far because they only exported one function.
How can I export multiple functions? I also want the names to remain "fetchData" and "sendData"
EDIT:
I tried to apply the approaches of Iamhuynq and Bergi, but now something goes south. I am importing the functions first and foremost in
moduleUser.js and authUser.js which reside in /src/store/modules.
The authUser.js is used for the identification of the user, so of course it is used in the login screen. When I now try to login, I get "Type Error: Object undefined". I guess this is because the functions returning the server response are somehow failing or not found.
The codebase connected to this behavior is the Login screen, the db_handler which Ive already shown you and a module called "moduleAuth.js".
First, the login screen looks like this:
<template>
<div>
<h1>Login</h1>
<p>Name:</p>
<div class="inputbox">
<input ref="username" type='text' v-on:keydown.enter="userLogin">
</div>
<p>Password:</p>
<div class="inputbox">
<input class="inputbox" ref="password" type='password' v-on:keydown.enter="userLogin">
</div>
<p>{{error}}</p>
<button v-on:click='userLogin'>Login</button>
</div>
</template>
<script>
import store from "../store/store";
import { AUTH_REQUEST } from "../store/actions/auth";
export default {
data () {
return {
error: ""
}
},
methods: {
userLogin: function(){
this.error = '';
store.dispatch(AUTH_REQUEST,{username: this.$refs.username.value, password: this.$refs.password.value})
.then((res) => {
this.$router.push({path: '/profile'});
})
.catch((err) => {
this.error = err;
});
this.$refs.password.value = '';
}
}
}
</script>
<style>
.inputbox{
width: 25%;
}
</style>
moduleAuth.js, from which the AUTH_REQUEST vue-action is coming, looks like this:
import axios from "axios";
import Vue from 'vue';
import Vuex from 'vuex';
import {fetchData, sendData} from "../../utility/db_handler";
import { USER_REQUEST } from "../actions/user";
import { AUTH_REQUEST, AUTH_LOGOUT, AUTH_FAIL, AUTH_SUCCESS } from "../actions/auth";
import { METHOD_POST, JWT } from "../../utility/namespaces";
Vue.use(Vuex);
const storeAuth = {
state: {
token: localStorage.getItem(JWT) || '',
loginState: ''
},
getters: {
isAuthenticated: state => !!state.token,
getLoginState: state => state.loginState
},
mutations: {
[AUTH_REQUEST]: (state) => {
state.loginState = 'pending';
},
[AUTH_FAIL]: (state) => {
state.loginState = 'error';
},
[AUTH_SUCCESS]: (state, mToken) => {
state.loginState = '';
state.token = mToken;
},
[AUTH_LOGOUT]: (state) => {
return new Promise ((resolve, reject) =>{
state.loginState = '';
state.token = '';
localStorage.removeItem(JWT);
resolve();
//Catch?
})
}
},
actions: {
[AUTH_REQUEST]: ({ commit, dispatch }, uObj) => {
return new Promise((resolve, reject) => {
commit(AUTH_REQUEST);
fetchData(METHOD_POST, '/login',{
username: uObj.username,
password: uObj.password
}).then(function (res) {
commit(AUTH_SUCCESS,res.headers.authorization);
localStorage.setItem(JWT,res.headers.authorization);
axios.defaults.headers.common['Authorization'] = res.headers.authorization;
dispatch(USER_REQUEST);
resolve(res.data);
}).catch(function(err) {
commit(AUTH_FAIL);
reject(err);
})
})
},
[AUTH_LOGOUT]: ({ commit}) => {
commit(AUTH_LOGOUT);
}
}
}
export default storeAuth
Now, if just roll back the changes to the export/import sections, everything works. So the problem should definitely be connected to this.
you can use export
export function sendData() {...}
and you can import like this
import fetchData, { sendData } from '/src/utility/db_handler.js;'
Here my suggestion is, if you are exporting more then one function, you should use export method instead of export default. It will make your code more readable and ll use for future debugging.
export function function1(params) {
.......
}
export function function2() {
......
}
Here there is a two way to import functions
by using import { function1, function2} from "./exportedFunctionFile" make sure you are using same function name as you exported!
other method is use * as yourVariableName example import * as myFunctions from "./exportedFunctionFile" this would use when you are exporting too many functions now you can use your imported functions as myfunctions.function1()
if you want to export using default key word, export functions as object example export default {function1,function2} and you could use it like import * as myFunctions from "./exportedFunctionFile" which is similar as a second way of importion.
Hope it will Help you
export the functions in an object
export default {
sendData: sendData,
fetchData: fetchData
}
then to use
import * as DBHandler from '#/src/utility/db_handler'
...
DBHandler.sendData()
On the function files
func1(params) {
...
}
func2(params) {
...
}
export default default {
function1: function1,
function2: function2
}
On the other file
import * as _ from './module address'
then
_.default.func1.call(args)
_.default.func2.call(args)
I am trying to send my vuejs front end data using Vuex and Vue-axios to the backend. I have create a vuex store and vue-axios services But I get an error saying [vuex] unknown action type: addGeneral when I try to pass the data.
This is my vuex folder structure:
-store
-modules
-app
-mutations.js
-state.js
-general.js
-index.js
-actions.js
-getters.js
-index.js
-mutations.js
-state.js
This is module/general.js :
import { ApiService } from '#/services/api.service'
import { FETCH_GENERAL,
ADD_GENERAL
} from '../actions'
import { FETCH_START,
FETCH_END,
SET_GENERAL,
SET_ERROR,
} from '../mutations'
const state = {
general: [],
errors: {},
loading: false
};
const getters = {
general (state) {
return state.general;
},
isLoading (state) {
return state.loading;
}
};
const actions = {
[FETCH_GENERAL] (context, payload) {
context.commit(FETCH_START);
return ApiService
.get('general')
.then(({data}) => {
context.commit(FETCH_END);
context.commit(SET_GENERAL, data.general.results);
})
.catch(({response}) => {
context.commit(SET_ERROR, response.data.errors)
})
},
[ADD_GENERAL] (context, payload) {
context.commit(FETCH_START);
return ApiService
.postGeneral(`general`, '',payload)
.then(({data}) => {
context.commit(FETCH_END);
context.commit(SET_GENERAL, data.general.results);
})
.catch(({response}) => {
context.commit(SET_ERROR, response.data.errors)
})
}
};
const mutations = {
[FETCH_START] (state) {
state.loading = true
},
[FETCH_END] (state) {
state.loading = false
},
[SET_GENERAL] (state, pgeneral) { // can pass in payload
state.components = pgeneral;
state.errors = {}
},
[SET_ERROR] (state, errors) {
state.errors = errors
}
};
export default {
state,
getters,
actions,
mutations
}
This is module/index.js :
const requireModule = require.context('.', true, /\.js$/)
const modules = {}
requireModule.keys().forEach(fileName => {
if (fileName === './index.js') return
// Replace ./ and .js
const path = fileName.replace(/(\.\/|\.js)/g, '')
const [moduleName, imported] = path.split('/')
if (!modules[moduleName]) {
modules[moduleName] = {
namespaced: true
}
}
modules[moduleName][imported] = requireModule(fileName).default
})
export default modules
This is store/actions.js :
export const FETCH_GENERAL = "fetchGeneral";
export const ADD_GENERAL = "addGeneral";
This is store/index.js :
import Vue from 'vue'
import Vuex from 'vuex'
// Store functionality
import actions from './actions'
import getters from './getters'
import modules from './modules'
import mutations from './mutations'
import state from './state'
Vue.use(Vuex)
// Create a new store
const store = new Vuex.Store({
actions,
getters,
modules,
mutations,
state
})
export default store
This is store/mutations.js :
export const FETCH_START = "loadingOn";
export const FETCH_END = "loadingOff";
export const SET_ERROR = "setError";
// related to general
export const SET_GENERAL = 'setGeneral';
This is my vue-axios folder structure:
-services
-api.services.js
-config.js
This is services/api.serviecs.js :
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
import { API_URL } from './config'
import Cookies from 'js-cookie'
let CSRF_TOKEN = Cookies.get('csrftoken');
export const ApiService = {
init () {
Vue.use(VueAxios, axios)
Vue.axios.defaults.baseURL = API_URL
},
get (resource, slug='') {
return Vue.axios
.get(`${resource}\${slug}`,)
.catch((error) => {
throw new Error(`ApiService ${error}`)
})
},
postGeneral (resource, slug='', obj) {
return axios
.post(`${API_URL}\\${resource}\\${slug}`,{
systemName: obj.systemName,
regionOfDeployment: obj.regionOfDeployment,
operatingMode: obj.operatingMode,
solution: obj.solution,
baselineMode: obj.baselineMode,
baselineDetails: obj.baselineDetails,
projectDuration: obj.projectDuration,
},
{
headers: {
'X-CSRFToken': CSRF_TOKEN,
'Content-Type': 'application/json',
}
})
.catch((error) => {
throw new Error (`ApiService ${error}`)
})
},
}
export default ApiService
This is config.js:
export default {}
export const API_URL = 'http://127.0.0.1:8000/api';
and finally this is my vuejs component:
...
<v-btn class="mt-5 mr-2 font-weight-light" color="blue"
#click="addGeneral" >
...
methods: {
addGeneral() {
let obj = {
systemName: '',
regionOfDeployment: '',
operatingMode: '',
solution: '',
baselineMode: '',
baselineDetails: '',
projectDuration: ''
};
this.postGeneral(obj)
},
postGeneral(obj) {
this.$store.dispatch(ADD_GENERAL, obj)
}
}
Why do I get the error and what's the best way to solve it?
You're using namespaced: true, so you need to pass module name in dispatch
postGeneral(obj) {
this.$store.dispatch('general/' + ADD_GENERAL, obj)
}
I have problem about api fetch with vuex. And there is no problem with my endpoint. I can see the json data. But when I try to fetch it i can't store the data and displaying console error below.
Error in mounted hook: "TypeError:
_api_article_js__WEBPACK_IMPORTED_MODULE_0__.default.getArticles is not a function"
About my import and export:
App.js
window._ = require('lodash');
try {
window.$ = window.jQuery = require('jquery');
require('foundation-sites');
} catch (e) {}
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
import Vue from 'vue';
import router from './routes.js';
import store from './store.js';
new Vue({
router,
store,
}).$mount('#app')
config.js
var api_url = 'mywebsite.com/api';
export const ESTATE_CONFIG = {
API_URL: api_url,
}
api/article.js
import { ESTATE_CONFIG } from '../config.js';
export default {
getarticles: function(){
return axios.get( ESTATE_CONFIG.API_URL + '/articles' );
},
}
Store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import { articles } from './modules/articles.js'
export default new Vuex.Store({
modules: {
articles,
}
});
modules/articles.js
import ArticleAPI from '../api/article.js';
export const articles = {
state: {
articles: [],
articlesLoadStatus: 0,
article: {},
articleLoadStatus: 0
},
getters: {
getArticlesLoadStatus( state ){
return state.articlesLoadStatus;
},
getArticles( state ){
return state.articles;
},
},
mutations: {
setArticlesLoadStatus( state, status ){
state.articlesLoadStatus = status;
},
setArticles( state, articles ){
state.articles = articles;
},
},
actions: {
loadArticles( { commit } ){
commit( 'setArticlesLoadStatus', 1 );
ArticleAPI.getArticles()
.then( function( response ){
commit( 'setArticles', response.data );
commit( 'setArticlesLoadStatus', 2 );
})
.catch( function(){
commit( 'setArticles', [] );
commit( 'setArticlesLoadStatus', 3 );
});
},
},
}
I need help about this. Because I am not sure what I am doing wrong here. And of course there is no problem with the endpoint. I can see the json data. But my vuex store is empty. And I have an error above.
The error indicates that an exported function called getArticles does not exist in api/article.js.
Taking a look at that module, it looks like a capitalization issue. The function is not capitalized so when calling it, use:
ArticleAPI.getarticles
Studying Vuex. I wrote a simple login page against the example project and the document, but when I tried to use a action function, the developer tool just warned me
Here is my code:
src/views/Login.vue
handleLogin (formName) {
this.$refs[formName].validate(valid => {
if (valid) {
// to do
this.$store.dispatch('user/login', this.loginUser)
} else {
......
})
}
})
src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/User/user'
// import info from './modules/info'
Vue.use(Vuex)
export default new Vuex.Store({
strict: false,
modules: {
user,
// info
}
})
/src/store/modules/User/actions.js
export const userActions = {
login({commit}, loginUser) {
commit(LOGIN)
axios.post(`${ API_BASE_USER }/login`, loginUser)
.then(res => {
console.log(res)
if (res.status == 200) { commit(LOGIN_SUCCESS, res.data) }
else { commit(LOGIN_FAILURE, res.data) }
})
}
}
/src/store/modules/User/user.js
import { userActions } from './actions'
import { userMutations } from './mutations'
export default {
namespaced: true,
state: {
token: ''
},
actions: Object.assign({}, userActions),
mutations: Object.assign({}, userMutations)
}
I got it.
The origin mutations-type.js export const LOGIN = LOGIN
But the correct mutation-type.js should be export const LOGIN = 'LOGIN'
This can also happen when you call $store.commit() without providing it an argument
Had a similar situation, where the Mutation name started with a CAP (), but the actual mutation started with a non cap():
RetrieveContractorSuccess: 'retrieveContractorSuccess'
(before was RetrieveContractorSuccess: 'RetrieveContractorSuccess')