VueX: Don't nest state with modules? - vue.js

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

Related

How can I bind a ref to a Pinia state variable through an action, in Vue 3?

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 want to update my component's state on vuex state change

This is my task module which I am using in store. In this my showTaskInForm() function works perfectly fine. I can see the task appear in state.task through vue dev tools
const state = {
tasks: [] // array of objects
task: null
}
const getters = {
task: state => state.task
};
const actions = {
showTaskInForm({ commit, state }, taskId) {
const task = state.tasks.filter(task => task.id === taskId);
const taskToEdit = task.length === 1 && task[0];
commit('showTaskInForm', taskToEdit);
}
};
const mutations = {
showTaskInForm: (state, taskToEdit) => (state.task = taskToEdit)
}
export default {
state,
getters,
actions,
mutations
};
And this is my AddTask.vue component
import { mapActions, mapGetters } from "vuex";
export default {
name: "AddTask",
data() {
return {
taskName : this.task ? this.task.name : null,
description : this.task ? this.task.description : null,
};
},
computed: mapGetters(['task']),
};
As I mentioned earlier I can see the state.task in vuex is being filled but in my AddTask.vue component I need to make that if there is task in vuex so my form fields get the value of the state.task.
Help me please I am new to Vue.js.
The object returned by the data function in your AddTask component is evaluated only once when the component is instantiated. That means that your expressions for taskName and description won't evaluate during the life cycle of the object.
You need to model taskName and description as computed properties in order to the tertiary operator to be evaluated after the dependency changes (in your case, the value of task coming from the store).
import { mapActions, mapGetters } from "vuex";
export default {
name: "AddTask",
computed: {
...mapGetters(['task']),
taskName() {
return this.task ? this.task.name : null
},
description() {
return this.task ? this.task.description : null
}
};

Vuex: createNamespacedHelpers with dynamic namespace

In almost all guides, tutorial, posts, etc that I have seen on vuex module registration, if the module is registered by the component the createNamespacedHelpers are imported and defined prior to the export default component statement, e.g.:
import {createNamespacedHelpers} from 'vuex'
const {mapState} = createNamespacedHelpers('mymod')
import module from '#/store/modules/mymod'
export default {
beforeCreated() {
this.$store.registerModule('mymod', module)
}
}
this works as expected, but what if we want the module to have a unique or user defined namespace?
import {createNamespacedHelpers} from 'vuex'
import module from '#/store/modules/mymod'
export default {
props: { namespace: 'mymod' },
beforeCreated() {
const ns = this.$options.propData.namespace
this.$store.registerModule(ns, module)
const {mapState} = createNamespacedHelpers(ns)
this.$options.computed = {
...mapState(['testVar'])
}
}
}
I thought this would work, but it doesnt.
Why is something like this needed?
because
export default {
...
computed: {
...mapState(this.namespace, ['testVar']),
...
},
...
}
doesnt work
This style of work around by utilising beforeCreate to access the variables you want should work, I did this from the props passed into your component instance:
import { createNamespacedHelpers } from "vuex";
import module from '#/store/modules/mymod';
export default {
name: "someComponent",
props: ['namespace'],
beforeCreate() {
let namespace = this.$options.propsData.namespace;
const { mapActions, mapState } = createNamespacedHelpers(namespace);
// register your module first
this.$store.registerModule(namespace, module);
// now that createNamespacedHelpers can use props we can now use neater mapping
this.$options.computed = {
...mapState({
name: state => state.name,
description: state => state.description
}),
// because we use spread operator above we can still add component specifics
aFunctionComputed(){ return this.name + "functions";},
anArrowComputed: () => `${this.name}arrows`,
};
// set up your method bindings via the $options variable
this.$options.methods = {
...mapActions(["initialiseModuleData"])
};
},
created() {
// call your actions passing your payloads in the first param if you need
this.initialiseModuleData({ id: 123, name: "Tom" });
}
}
I personally use a helper function in the module I'm importing to get a namespace, so if I hadmy module storing projects and passed a projectId of 123 to my component/page using router and/or props it would look like this:
import projectModule from '#/store/project.module';
export default{
props['projectId'], // eg. 123
...
beforeCreate() {
// dynamic namespace built using whatever module you want:
let namespace = projectModule.buildNamespace(this.$options.propsData.projectId); // 'project:123'
// ... everything else as above
}
}
Hope you find this useful.
All posted answers are just workarounds leading to a code that feels verbose and way away from standard code people are used to when dealing with stores.
So I just wanted to let everyone know that brophdawg11 (one of the commenters on the issue #863) created (and open sourced) set of mapInstanceXXX helpers aiming to solve this issue.
There is also series of 3 blog posts explaining reasons behind. Good read...
I found this from veux github issue, it seems to meet your needs
https://github.com/vuejs/vuex/issues/863#issuecomment-329510765
{
props: ['namespace'],
computed: mapState({
state (state) {
return state[this.namespace]
},
someGetter (state, getters) {
return getters[this.namespace + '/someGetter']
}
}),
methods: {
...mapActions({
someAction (dispatch, payload) {
return dispatch(this.namespace + '/someAction', payload)
}
}),
...mapMutations({
someMutation (commit, payload) {
return commit(this.namespace + '/someMutation', payload)
})
})
}
}
... or maybe we don't need mapXXX helpers,
mentioned by this comment https://github.com/vuejs/vuex/issues/863#issuecomment-439039257
computed: {
state () {
return this.$store.state[this.namespace]
},
someGetter () {
return this.$store.getters[this.namespace + '/someGetter']
}
},

Why am I getting "rawModule is undefined" when adding Vuex modules?

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

Eslint state already declared [Vuex]

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"] }]*/