I recently was struggling with implementing modules in Vuex for the first time. I couldn't find much info on the console error message I was getting ( rawModule is undefined ), so I thought I'd share the issue I ran into and the solution. I was doing a quick, simple version of a module implementation as I was working through some examples:
export const store = new Vuex.Store({
state: {
loggedIn: false,
user: {},
destination: ''
},
mutations: {
login: state => state.loggedIn = true,
logout: state => state.loggedIn = false,
updateUser: ( state, user ) => { state.user = user },
updateDestination: ( state, newPath ) => { state.destination = newPath }
},
modules: {
project
},
});
const project = {
state: {}
}
The issue ultimately was that I had declared my module after I tried to add it to the Vuex store. I had thought it would have been okay to declare the module later thanks to variable hoisting, but that doesn't appear to be the case. Here is the code that does work:
const project = {
state: {}
}
export const store = new Vuex.Store({
state: {
loggedIn: false,
user: {},
destination: ''
},
mutations: {
login: state => state.loggedIn = true,
logout: state => state.loggedIn = false,
updateUser: ( state, user ) => { state.user = user },
updateDestination: ( state, newPath ) => { state.destination = newPath }
},
modules: {
project
},
});
Hopefully this saves some people some time. I didn't see anything in the documentation requiring a certain ordering, so I'm surprised it mattered. If anyone has some insight into why it works this way, I'd be really interested in hearing it! Perhaps because the Vuex.Store() function gets called before the project value is set, so the project module's value is encapsulated as undefined, and that causes the error?
If you have using class components, put the store import before the module import, Eg:
// This is wrong
import { getModule } from "vuex-module-decorators";
import AppModule from "#/store/app-module";
import store from "#/store";
const appModule = getModule(AppState, store);
// This work
import { getModule } from "vuex-module-decorators";
import store from "#/store"; // Before first
import AppModule from "#/store/app-module"; // Then import the module
const appModule = getModule(AppState, store);
Related
I'm using the useFirestore composable from the vueUse library - I had success in reactively binding my "titles" document to the titles store variable, however when I try to bind userData through an action nothing happens (note: my Firebase config is fine).
What's the correct way to do this?
// user.store.js
import { defineStore } from "pinia";
import { useFirestore } from "#vueuse/firebase/useFirestore";
import { db, doc } from "../../../config/firebase";
export const useUserStore = defineStore("user", {
state: () => ({
titles: useFirestore(doc(db, "titles", "available")), // <-- this works and binds the firestore document
userData: null,
}),
getters: {
getUserData: (state) => state.userData,
},
actions: {
setUserData(uid) {
this.userData = useFirestore(doc(db, "users", uid)); // <-- this doesn't do anything and userData is `null` in the dev tools.
},
}
});
// Component.vue
...
setUserData("my-id");
Ah, I neglected to use $patch. This worked for me:
setUserData(uid) {
const user = useFirestore(doc(db, "users", uid));
this.$patch({ userData: user });
}
If I'm using this in the wrong way, please let me know.
I have the following directory structure:
store
modules
file1.js
file2.js
file3.js
anotherFolder
filex1.js
My question is, how to access the state of anotherFolder/filex1.js
I can set the value, but I can't get it.
this.$store.dispatch('anotherFolder/filex1/myAction', {}) //work
let val = this.$store.state.anotherFolder.filex1.myValue //not work
I am importing the modules as follows.
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
Vue.use(Vuex)
const modulesFiles = require.context('./modules', true, /\.js$/)
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {})
const store = new Vuex.Store({
modules,
getters
})
export default store
my filex1.js
export default {
namespaced: true,
state: {
myValue: {}
},
actions: {
myAction({ commit }, reg) {
commit('MY_ACTION', reg)
}
},
mutations: {
MY_ACTION(state, reg) {
state.myValue = reg
}
}
}
I want to organize the store's files in subdirectories so I don't have everything in the modules folder.
thanks!
you could use the binding helpers provided by vuex in your component to access the state in a nested module:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
}
see:
https://vuex.vuejs.org/guide/modules.html#namespacing
I'm using a helper function to create a store inside my jests. The helper function uses deepmerge to merge the basic configuration with a customized configuration. This results in multiple console warnings
[vuex] state field "cart" was overridden by a module with the same name at "cart"
[vuex] state field "customer" was overridden by a module with the same name at "customer"
[vuex] state field "checkout" was overridden by a module with the same name at "checkout"
store.js (Reduced to a minimum for presentation purpose)
import cart from './modules/cart'
import checkout from './modules/checkout'
import customer from './modules/customer'
Vue.use(Vuex)
export const config = {
modules: {
cart,
customer,
checkout,
},
}
export default new Vuex.Store(config)
test-utils.js
import merge from 'deepmerge'
import { config as storeConfig } from './vuex/store'
// merge basic config with custom config
export const createStore = config => {
const combinedConfig = (config)
? merge(storeConfig, config)
: storeConfig
return new Vuex.Store(combinedConfig)
}
making use of the helper function inside
somejest.test.js
import { createStore } from 'test-utils'
const wrapper = mount(ShippingComponent, {
store: createStore({
modules: {
checkout: {
state: {
availableShippingMethods: {
flatrate: {
carrier_title: 'Flat Rate',
},
},
},
},
},
}),
localVue,
})
How do I solve the console warning?
I believe the warning is somewhat misleading in this case. It is technically true, just not helpful.
The following code will generate the same warning. It doesn't use deepmerge, vue-test-utils or jest but I believe the root cause is the same as in the original question:
const config = {
state: {},
modules: {
customer: {}
}
}
const store1 = new Vuex.Store(config)
const store2 = new Vuex.Store(config)
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<script src="https://unpkg.com/vuex#3.4.0/dist/vuex.js"></script>
There are two key parts of this example that are required to trigger the warning:
Multiple stores.
A root state object in the config.
The code in the question definitely has multiple stores. One is created at the end of store.js and the other is created by createStore.
The question doesn't show a root state object, but it does mention that the code has been reduced. I'm assuming that the full code does have this object.
So why does this trigger that warning?
Module state is stored within the root state object. Even though the module in my example doesn't explicitly have any state it does still exist. This state will be stored at state.customer. So when the first store gets created it adds a customer property to that root state object.
So far there's no problem.
However, when the second store gets created it uses the same root state object. Making a copy or merging the config at this stage won't help because the copied state will also have the customer property. The second store also tries to add customer to the root state. However, it finds that the property already exists, gets confused and logs a warning.
There is some coverage of this in the official documentation:
https://vuex.vuejs.org/guide/modules.html#module-reuse
The easiest way to fix this is to use a function for the root state instead:
state: () => ({ /* all the state you currently have */ }),
Each store will call that function and get its own copy of the state. It's just the same as using a data function for a component.
If you don't actually need root state you could also fix it by just removing it altogether. If no state is specified then Vuex will create a new root state object each time.
It is logged when a property name within the state conflicts with the name of a module, like so:
new Vuex.Store({
state: {
foo: 'bar'
},
modules: {
foo: {}
}
})
therefore this raises the warning.
new Vuex.Store(({
state: {
cart: '',
customer: '',
checkout: ''
},
modules: {
cart: {},
customer: {},
checkout: {},
}
}))
its most likely here
export const createStore = config => {
const combinedConfig = (config)
? merge(storeConfig, config)
: storeConfig
return new Vuex.Store(combinedConfig)
}
from the source code of vuex, it helps indicate where these errors are being raised for logging.
If you run the app in production, you know that this warning wont be raised... or you could potentially intercept the warning and immediately return;
vuex source code
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
if (__DEV__) {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
Vue.set(parentState, moduleName, module.state)
})
vuex tests
jest.spyOn(console, 'warn').mockImplementation()
const store = new Vuex.Store({
modules: {
foo: {
state () {
return { value: 1 }
},
modules: {
value: {
state: () => 2
}
}
}
}
})
expect(store.state.foo.value).toBe(2)
expect(console.warn).toHaveBeenCalledWith(
`[vuex] state field "value" was overridden by a module with the same name at "foo.value"`
)
Well, I believe there is no need for using deepmerge in test-utils.ts. It is better we use Vuex itself to handle the merging of the module instead of merging it with other methods.
If you see the documentation for Vuex testing with Jest on mocking modules
you need to pass the module which is required.
import { createStore, createLocalVue } from 'test-utils';
import Vuex from 'vuex';
const localVue = createLocalVue()
localVue.use(Vuex);
// mock the store in beforeEach
describe('MyComponent.vue', () => {
let actions
let state
let store
beforeEach(() => {
state = {
availableShippingMethods: {
flatrate: {
carrier_title: 'Flat Rate',
},
},
}
actions = {
moduleActionClick: jest.fn()
}
store = new Vuex.Store({
modules: {
checkout: {
state,
actions,
getters: myModule.getters // you can get your getters from store. No need to mock those
}
}
})
})
});
whistle, In test cases:
const wrapper = shallowMount(MyComponent, { store, localVue })
Hope this helps!
So, I love the idea of VueX modules and separating my data out, as it makes it far easier to reason when there are large sets of data... but I hate having to refer to them as nested objects in the store's state.
This is how the module currently works:
contactData.js:
export const contactData = {
state: {
contactInfo: null,
hasExpiredContacts: false
},
mutations: {
updateContactInfo(state, data) {
state.contactInfo = data;
},
updateExpired(state, data) {
state.hasExpiredContacts = data;
}
}
}
store.js:
import Vue from 'vue';
import Vuex from 'vuex';
import { contactData } from './contactData.js';
Vue.use(Vuex);
export default new Vuex.Store({
modules: { contactData },
state: {
otherData: null
}
});
Which would return as:
store: {
state: {
contactData: {
contactInfo: null,
hasExpiredContacts: false
},
otherData: null
}
}
Is there anyway to, instead, display it as the following, while still using a module?
store: {
state: {
contactInfo: null,
hasExpiredContacts: false,
otherData: null
}
}
I'm not sure that flattening out all your state would necessarily be a great idea if the project grew larger, as you'd have to be wary of property name clashes.
However, ignoring that, you could perhaps create flat getters for all module state automatically. Since this just provides alternative access all actions and mutations will work in the normal way.
const modules = {
contactData,
user,
search,
...
}
const flatStateGetters = (modules) => {
const result = {}
Object.keys(modules).forEach(moduleName => {
const moduleGetters = Object.keys(modules[moduleName].getters || {});
Object.keys(modules[moduleName].state).forEach(propName => {
if (!moduleGetters.includes(propName)) {
result[propName] = (state) => state[moduleName][propName];
}
})
})
return result;
}
export const store = new Vuex.Store({
modules,
getters: flatStateGetters(modules),
state: {
otherData: null
}
})
Since there's no deep merge possible still in ES6/ES7, you can't do it like the way you want.
You need to make your own function or find a suitable library to deep merge the objects to make it work.
Here's a possible solution using lodash:
modules: { _.merge(contactData, { state: { otherData: null } } ) }
I am running ESLint and I am currently running into the following ESLint error:
error 'state' is already declared in the upper scope no-shadow
const state = {
date: '',
show: false
};
const getters = {
date: state => state.date,
show: state => state.show
};
const mutations = {
updateDate(state, payload) {
state.date = payload.date;
},
showDatePicker(state) {
state.show = true;
}
};
export default {
state,
getters,
mutations
};
What would be the best way to fix this?
The best way to fix would be to read the docs about the eslint "no-shadow" rule.
From this documentation, the best solution would probably be to include an exception for this one variable with the "allow" option.
You can add this with a comment to the js file to keep the exeption local:
/* eslint no-shadow: ["error", { "allow": ["state"] }]*/
The best solution is #Linus Borg's answer.
If you are looking for an alternative, you can declare the state constant below the rest. This will prevent variable shadowing because state will not be declared in the outer-scope yet.
Example:
const getters = {
date: state => state.date,
show: state => state.show
};
const mutations = {
updateDate(state, payload) {
state.date = payload.date;
},
showDatePicker(state) {
state.show = true;
}
};
const state = {
date: '',
show: false
};
export default {
state,
getters,
mutations
};
If it's not too late
const data = {
date: '',
show: false
};
const getters = {
date: state => state.date,
show: state => state.show
};
const mutations = {
updateDate(state, payload) {
state.date = payload.date;
},
showDatePicker(state) {
state.show = true;
}
};
export default {
state: data,
getters,
mutations
};
basically you define your store data as data, and you export it as state state: data
Had the same issue as I was using an airbnb eslint config which is incompatible with vuex.
This worked for me, after restarting dev environment.
I created a new .eslintrc.js file in my store folder and added them there
"no-shadow": ["error", { "allow": ["state"] }],
"no-param-reassign": [
"error",
{
"props": true,
"ignorePropertyModificationsFor": [ // All properties except state are in the ignorePropertyModificationsFor array by default.
"state",
"acc",
"e",
"ctx",
"req",
"request",
"res",
"response",
"$scope"
]
}
],
Based on #allochi's answer, this is what I had to do to make it work With Vue 3 which uses Vuex 4 which prefers returning a function for state:
// store.js
const data = {
// ...
};
const getters = {
// ...
};
const mutations = {
// ...
};
const actions = {
// ...
};
export default {
state() { return data; },
getters,
mutations,
actions
};
If you need to import particular functions from outside, you will have to do it like this:
import mystore from './mystore';
const Store = createStore({
state: mystore.state,
getters: mystore.getters,
mutations: mystore.mutations,
actions: mystore.actions
});
I would only recommend this though if you really can't use /* eslint no-shadow: ["error", { "allow": ["state"] }]*/