Using mapActions correctly with nuxt.js - vue.js

I need to use mapActions in a nuxt.js store.
If I call the "dispatch" method all is ok, but when I use the mapActions I get 'Maximum call stack size exceeded' error.
// storequiz.js
export const state = () => ({
quizVersion: null,
stage: null,
title: null,
img: null,
questions: [],
currentQuestion: null,
answers: [],
})
export const mutations = {
setQuizVersion(state, version) {
state.quizVersion = version
},
setQuestions(state, questions) {
state.questions = questions
},
}
export const actions = {
async fetchData({ commit }, payload) {
const res = await this.$axios.get(payload)
commit('setQuizVersion', res.data.version)
commit('setQuestions', res.data.questions)
},
}
// my methods:
methods: {
...mapActions({
fetchData: 'storequiz/fetchData',
}),
async fetchData() {
// await this.$store.dispatch('storequiz/fetchData', this.url)
await this.fetchData(this.url)
},
....
I am getting the error:
error: status code 404, net::ERR_HTTP_RESPONSE_CODE_FAILURE
client.js?06a0:103 RangeError: Maximum call stack size exceeded
at new Context (runtime.js?96cf:449)
at Object.wrap (runtime.js?96cf:41)
at _callee2 (Quiz.vue?b7d7:182)
at eval (asyncToGenerator.js?1da1:22)
at new Promise (<anonymous>)
at eval (asyncToGenerator.js?1da1:21)
at VueComponent.fetchData (Quiz.vue?b7d7:182)
at _callee2$ (Quiz.vue?b7d7:182)
at tryCatch (runtime.js?96cf:63)
at Generator.invoke [as _invoke] (runtime.js?96cf:293)
If I use: await this.$store.dispatch('storequiz/fetchData', this.url)
instead of await this.fetchData(this.url), all works well.
What am I missing here?

The mapped fetchData is hidden by the subsequent method of the same name. You should rename one of them for unique method names (if they're both needed).
Also, the component method fetchData calls itself unconditionally, so there's an infinite loop that would lead to the Maximum call stack size exceeded. Add a condition to break out of the loop (if this call is even needed).

Related

Uncaught (in promise) TypeError: Cannot create property 'token' on string

Am trying to implement authentication using vuex. I have a register component and also a auth.js for submiting data.
My backend API are working fine but the issue is when i try to login. When i console log in action method am able to get the token.
when i console log before the SET_TOKEN mutation am also able to get data.
But in mutations the data is received like an object. How do I solve the error?
import axios from "axios"
export default{
namespaced: true,
state: {
token: null,
user: null
},
mutations: {
SET_TOKEN(access_token, state){
console.log(this, access_token)
state.token = this,access_token
},
},
actions: {
async loginUser({ dispatch }, form) {
let response = await axios.post('/api/auth/login', form)
dispatch('attempt', response.data.data.access_token)
},
async attempt({ commit }, access_token){
commit('SET_TOKEN', access_token)
}
}
}
I think you mixed up the order of mutation parameters a little bit. First is state, second is payload (token in your case)
SET_TOKEN(state, token) {
state.token = token;
},

Websocket within Vuex module: Async issue when trying to use Vuex rootstate

I'm trying to populate my app with data coming from a websocket in the most modular way possible trying to use best practices etc. Which is hard because even when I have dig very deep for advice on the use of websockets / Vuex and Vue I still can't find a pattern to get this done. After going back and forth I have decided to use a store to manage the state of the websocket and then use that vuex module to populate the state of other components, basically a chat queue and a chat widget hence the need to use websockets for real time communication.
This is the websocket store. As you can see I'm transforming the processWebsocket function into a promise in order to use async/await in other module store actions. The way I see this working (and I'm prob wrong, so PLEASE feel free to correct me) is that all the components that will make use of the websocket module state will wait until the state is ready and then use it (this is not working at the moment):
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChatInfo: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('xyz')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
if (wsData.connectionId && wsData.connectionId !== state.connectionId) {
state.connectionId = wsData.connectionId
console.log(`Retrieving Connection ID ${state.connectionId}`)
} else {
state.messageType = wsData.type
state.incomingChatInfo = wsData.documents
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
When I debug the app everything is working fine with the websocket store, I can see its state, the data from the server is there etc. The problem comes when I try to populate other components properties using the websocket. By the time other components need the websocket state this is not ready yet so I'm getting errors. Here's an example of one of my components trying to use the websocket state, I basically call an action from the created cycle method:
<template>
<ul class="overflow-y-auto overflow-hidden pr-2">
<BaseChat
v-for="(chat, index) in sortingIncomingChats"
:key="index"
:chat="chat"
:class="{ 'mt-0': index === 0, 'mt-4': index > 0 }"
/>
</ul>
</template>
<script>
import { mapState } from 'vuex'
import BaseChat from '#/components/BaseChat.vue'
export default {
components: {
BaseChat,
},
created() {
this.$store.dispatch('chatQueue/fetchChats')
},
data() {
return {
currentSort: 'timeInQueue',
currentSortDir: 'desc',
chats: [],
}
},
computed: {
sortingIncomingChats() {
return this.incomingChats.slice().sort((a, b) => {
let modifier = 1
if (this.currentSortDir === 'desc') modifier = -1
if (a[this.currentSort] < b[this.currentSort])
return -1 * modifier
if (a[this.currentSort] > b[this.currentSort])
return 1 * modifier
return 0
})
},
},
}
</script>
This is the chatQueue Vuex module that have the fetchChats action to populate data from the websocket to the APP:
export const namespaced = true
export const state = () => ({
incomingChats: [],
error: '',
})
export const actions = {
fetchChats({ commit, rootState }) {
const data = rootState.websocket.incomingChats
commit('SET_CHATS', data)
},
}
export const mutations = {
SET_CHATS(state, data) {
state.incomingChats = data
},
SET_ERROR(state, error) {
state.incomingChats = error
console.log(error)
},
}
This is where I get errors because "rootState.websocket.incomingChats" is not there yet when its called by the fetchChats module action, so I get:
TypeError: Cannot read properties of undefined (reading 'slice')
I have tried to transform that action into an async / await one but it's not working either, but as I mentioned I'm really new to async/await so maybe I'm doing something wrong here:
async fetchChats({ commit, rootState }) {
const data = await rootState.websocket.incomingChats
commit('SET_CHATS', data)
},
Any help will be really appreciated.
In case somebody have the same problem what I ended up doing is adding a getter to my websocket module:
export const getters = {
incomingChats: (state) => {
return state.incomingChatInfo
},
}
And then using that getter within a computed value in the component I need to populate with the websocket component.
computed: {
...mapGetters('websocket', ['incomingChats']),
},
And I use the getter on a regular v-for loop within the component:
<BaseChat
v-for="(chat, index) in incomingChats"
:key="index"
:chat="chat"
:class="{ 'mt-0': index === 0, 'mt-4': index > 0 }"
/>
That way I don't have any kind of sync problem with the websocket since I'm sure the getter will bring data to the component before it tries to use it.

TypeError: Cannot read property 'cache' of undefined - VueJS

I created a Vue component which exports an async function. This component acts as a wrapper for calling my API. It's based on axios with a caching component that relies on localforage for some short lived persistence.
import localforage from 'localforage'
import memoryDriver from 'localforage-memoryStorageDriver'
import { setup } from 'axios-cache-adapter'
export default {
async cache() {
// Register the custom `memoryDriver` to `localforage`
await localforage.defineDriver(memoryDriver)
// Create `localforage` instance
const store = localforage.createInstance({
// List of drivers used
driver: [
localforage.INDEXEDDB,
localforage.LOCALSTORAGE,
memoryDriver._driver
],
// Prefix all storage keys to prevent conflicts
name: 'tgi-cache'
})
// Create `axios` instance with pre-configured `axios-cache-adapter` using a `localforage` store
return setup({
// `axios` options
baseURL: 'https://my.api',
cache: {
maxAge: 2 * 60 * 1000, // set cache time to 2 minutes
exclude: { query: false }, // cache requests with query parameters
store // pass `localforage` store to `axios-cache-adapter`
}
})
}
}
Here is how I am importing and using this component in my views:
import api from '#/components/Api.vue'
export default {
data() {
return {
userId: this.$route.params.id,
userData: ''
}
},
methods: {
loadClient(userId) {
const thisIns = this;
api.cache().then(async (api) => {
const response = await api.get('/client/find?id='+userId)
thisIns.userData = response.data.data[0]
}).catch(function (error) {
console.log(error)
})
},
},
created() {
this.loadClient(this.userId)
},
}
I can import this component and everything appears to work. I get data back from my API. However, immediately after every call, I get an error:
TypeError: Cannot read property 'cache' of undefined
Which references this line:
api.cache().then(async (api) => {
I am unable to understand why this is happening, or what it means. The error itself indicates that the component I am importing is undefined, though that's clearly not the case; if it were, the API call would ultimately fail I would suspect. Instead, I am lead to believe that perhaps I am not constructing/exporting my async cache() function properly.
Upon further review, I don't actually understand why the author has implemented it the way he has. Why would you want to create an instance of localForage every single time you make an API call?
I've opted not to use a component and to only instantiate an instance of localForage once.
main.js
import localforage from 'localforage'
import memoryDriver from 'localforage-memoryStorageDriver'
import { setup } from 'axios-cache-adapter'
// Register the custom `memoryDriver` to `localforage`
localforage.defineDriver(memoryDriver)
// Create `localforage` instance
const localforageStore = localforage.createInstance({
// List of drivers used
driver: [
localforage.INDEXEDDB,
localforage.LOCALSTORAGE,
memoryDriver._driver
],
// Prefix all storage keys to prevent conflicts
name: 'my-cache'
})
Vue.prototype.$http = setup({
baseURL: 'https://my.api',
cache: {
maxAge: 2 * 60 * 1000, // set cache time to 2 minutes
exclude: { query: false }, // cache requests with query parameters
localforageStore // pass `localforage` store to `axios-cache-adapter`
}
})
the view
export default {
data() {
return {
userId: this.$route.params.id,
userData: ''
}
},
methods: {
loadClient(userId) {
const thisIns = this;
thisIns.$http.get('/client/find?id='+userId)
.then(async (response) => {
thisIns.userData = response.data.data[0]
})
.catch(function (error) {
console.log(error)
})
},
},
created() {
this.loadClient(this.userId)
},
}

Mock put requests with mock-axios-adapter

I have simple Vue component that fetches API key when it is created and key can be renewed by clicking on button:
<template>
<div>
<div>{{data.api_key}}</div>
<button ref="refresh-trigger" #click="refreshKey()">refresh</button>
</div>
</template>
<script>
export default {
created() {
axios.get(this.url).then((response) => {
this.data = response.data
})
}
methods: {
refreshKey() {
axios.put(this.url).then((response) => {
this.data = response.data
})
},
}
}
</script>
And I want to test it with this code:
import {shallowMount} from '#vue/test-utils';
import axios from 'axios';
import apiPage from '../apiPage';
import MockAdapter from 'axios-mock-adapter';
describe('API page', () => {
it('should renew API key it on refresh', async (done) => {
const flushPromises = () => new Promise(resolve => setTimeout(resolve))
const initialData = {
api_key: 'initial_API_key',
};
const newData = {
api_key: 'new_API_key',
};
const mockAxios = new MockAdapter(axios);
mockAxios.onGet('/someurl.json').replyOnce(200, initialData)
mockAxios.onPut('/someurl.json').replyOnce(200, newData);
const wrapper = shallowMount(api);
expect(wrapper.vm.$data.data.api_key).toBeFalsy();
await flushPromises()
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$data.data.api_key).toEqual(initialData.api_key);
done()
});
wrapper.find({ref: 'refresh-trigger'}).trigger('click');
wrapper.vm.$nextTick(() => {
console.log(mockAxios.history)
expect(wrapper.vm.$data.data.api_key).toEqual(newData.api_key);
expect(mockAxios.history.get.length).toBe(1);
expect(mockAxios.history.get[1].data).toBe(JSON.stringify(initialData));
expect(mockAxios.history.put.length).toBe(1);
done();
});
})
});
But it turns out only get request is mocked because i receive:
[Vue warn]: Error in nextTick: "Error: expect(received).toEqual(expected)
Difference:
- Expected
+ Received
- new_API_key
+ initial_API_key"
found in
---> <Anonymous>
<Root>
console.error node_modules/vue/dist/vue.runtime.common.dev.js:1883
{ Error: expect(received).toEqual(expected)
Even worse, console.log(mockAxios.history) returns empty put array:
{ get:
[ { transformRequest: [Object],
transformResponse: [Object],
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
validateStatus: [Function: validateStatus],
headers: [Object],
method: 'get',
url: '/admin/options/api.json',
data: undefined } ],
post: [],
head: [],
delete: [],
patch: [],
put: [],
options: [],
list: [] }
I tried to define mockAxios in describe block, and console.log it after iteration - and it turns out that put request was here. But not when I needed it. :)
What am i doing wrong? Maybe there are some ways to check if created callback was called and all async functions inside it are done? Maybe i'm using axios-mock wrong?
This test code should pass:
import {shallowMount, createLocalVue} from '#vue/test-utils';
import axios from 'axios';
import api from '#/components/api.vue';
import MockAdapter from 'axios-mock-adapter';
describe('API page', () => {
it('should renew API key it on refresh', async () => {
const flushPromises = () => new Promise(resolve => setTimeout(resolve))
const initialData = {
api_key: 'initial_API_key',
};
const newData = {
api_key: 'new_API_key',
};
const mockAxios = new MockAdapter(axios);
const localVue = createLocalVue();
mockAxios
.onGet('/someurl.json').reply(200, initialData)
.onPut('/someurl.json').reply(200, newData);
const wrapper = shallowMount(api, {
localVue,
});
expect(wrapper.vm.$data.data.api_key).toBeFalsy();
await flushPromises();
expect(wrapper.vm.$data.data.api_key).toEqual(initialData.api_key);
wrapper.find({ref: 'refresh-trigger'}).trigger('click');
await flushPromises();
console.log(mockAxios.history);
expect(wrapper.vm.$data.data.api_key).toEqual(newData.api_key);
expect(mockAxios.history.get.length).toBe(1);
expect(mockAxios.history.put.length).toBe(1);
})
});
A few notes:
I prefer to chain the responses on the mockAxios object, that way you can group them by URL so it's clear which endpoint you're mocking:
mockAxios
.onGet('/someurl.json').reply(200, initialData)
.onPut('/someurl.json').reply(200, newData);
mockAxios
.onGet('/anotherUrl.json').reply(200, initialData)
.onPut('/anotherUrl.json').reply(200, newData);
If you want to test that you only made one GET call to the endpoint (with expect(......get.length).toBe(1)) then you should really use reply() instead of replyOnce() and test it the way you're doing it already. The replyOnce() function will remove the handler after replying first time and you'll be getting 404s in your subsequent requests.
mockAxios.history.get[1].data will not contain anything for 3 reasons: GET requests don't have a body (only URL parameters), you only made 1 GET request (here you're checking 2nd GET), and this statement refers to the request that was sent, not data you received.
You're using async/await feature, which means you can take advantage of that for $nextTick: await wrapper.vm.$nextTick(); and drop the done() call all together, but since you already have flushPromises() you might as well use that.
You don't need to test that you received initialData in the 1st call with this line:
expect(mockAxios.history.get[1].data).toBe(JSON.stringify(initialData)); since you're already testing it with expect(...).toEqual(apiKey).
Use createLocalVue() utility to create a local instance of Vue for each mount to avoid contaminating the global Vue instance (useful if you have multiple test groups)
and finally, 7. it's best to break this test up into multiple it statements; unit tests should be microtests, i.e. test a small, clearly identifiable behaviour. Although I didn't break the test up for you so it contains as little changes as possible, I'd highly recommend doing it.

How do correctly add a getter to a Vuex Module?

I'm trying to create a Vuex module whenever I register the module, I get a state is undefined, even though there is nothing calling the getter I had just made. I'm able to call actions correctly with no errors.
This is my module. customer.js
export default {
namespaced: true,
state: {
login: false,
},
getters: {
isLoggedIn: (state) => {
console.log(state);
state.login;
}
},
mutations: {
set_login: (state, login) => {
state.login = login;
},
set_orders: (state, orders) => {
state.orders = orders;
},
},
actions: {
newsletter_subscribe: (context, email) => {
//- TODO
},
}
}
I have register via the registerModule function.
import Customer from './customer';
store.registerModule('customer', Customer, {
preserveState: true
});
Whenever I have developer tools open it just alerts me that.
Uncaught ReferenceError: state is not defined
at isLoggedIn (customer.js:11)
at wrappedGetter (vuex.esm.js:734)
Am I doing anything wrong with my getter?
I've only noticed the getter being called with Vue Dev tools open as I tried putting an alert in the getter to see what else could be triggering without the state being passed in.
I managed to solve it by giving my store a blank state!
const store = new Vuex.Store({
state: {}
});
And registering modules as normal.