4
votes

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
})
3
the client will never know if the token is still validdevman

3 Answers

1
votes

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;
1
votes

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.

0
votes

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)
    )
  }
  // ...
}