Vuex 4 + Vue 3 using shared state actions in modules - vue.js

I have a Vue 3 app (without TypeScript), and I'm using a Vuex 4 store.
I have my store separated into modules that represent some common themes, for example I have a user module that contains the store, getters, actions etc that are appropriate for dealing with a user. But, I also have some common functionalities that is present in all of my modules, and it's an obvious place where I could simplify my modules and thin them out a bit.
Let's say for example that I have a generic set() mutator, like below:
const mutations = {
set(state, payload) {
state[payload.key] = payload.data;
}
}
This will simply take in a payload and populate whatever store object the payload 'key' field belongs to, and it works well when I want to simply set a single field in my store. Now, the issue is that this set() function is being duplicated in every single store module that I have since it's just a generic store mutation, and once the number of modules I reach increases a bit, you can imagine that it's quite wasteful and pointless to include this mutation every single time.
I'm not sure how to implement this, however.
My current main store file looks like this (simplified for the sake of the question):
// store/index.js
import { createStore } from "vuex";
import module1 from "./modules/module1";
import module2 from "./modules/module2";
export const store = createStore({
modules: { module1, module2 },
});
All of my modules are implemented in the same exact way:
// store/modules/module1.js
const state = { };
const mutations = { set(state, payload) };
const actions = { };
const getters = { };
export default {
namespaced: true,
state,
getters,
actions,
mutations
};
And then somewhere in a component or wherever I'd call the specific module action/mutation/store I need with the below:
...mapMutations: ({ set: "module1/set" })
What would be the best way for me to bring that shared functionality out into a singular place, and how would I properly use it if I, for example, wanted to set and mutate the store in module1 and module2 at the same time properly? The part that I'm not sure about is how exactly I could call a generic mutation and have it target the desired module's state

Thanks to this answer linked by #Beyers, I've implemented the store in a similar fashion to this:
// store/sharedModuleMethods.js
export default class {
constructor() {
this.mutations = {
set(state, payload) {}
}
}
}
And then in the modules, I just instantiate the class and spread the methods where they need to be
// store/modules/module1.js
import SharedModuleMethods from "../sharedModuleMethods";
const methods = new SharedModuleMethods()
const mutations = {
...methods.mutations,
// Module specific mutations go here
};
And then in whatever component I need to set() something, I can just call the mutation like I would before
...mapMutations: ({ setModule1: "module1/set", setModule2: "module2/set" })
It's not quite as automated and streamlined as I'd personally prefer (define it once, have the modules magically have all the methods available to them via a flag or something), but alas life isn't perfect either.

Related

sub-property write commit withing store actions using vuex-pathify make.mutations

Here is how my store looks like:
const state = {
user: {
profile: {
phoneNumber: '',
}
}
}
const mutations = make.mutations(state)
const actions = {
submitPhoneNumber({commit}, phone_number) {
// blah blah
commit('SET_USER#profile.phoneNumber', phone_number);
}
}
But no such mutation can be found.
Maybe I could import store.js within store.js and use the set helper but I believe things can get pretty creepy specially because of the (In my opinion poor) design decision that the library creator has made to combine commit and dispatch (I believe being explicit would have been much better here)
Pathify author here.
You can't commit a mutation with sub-property syntax using Vuex mutations, because Vuex will treat it as a string.
You are correct that you would need to use store.set() to do this.
You can be explicit with commits and dispatches by appending a ! to the call. This is called "direct syntax":
https://davestewart.github.io/vuex-pathify/#/api/paths?id=direct-syntax
To commit directly using the sub-property syntax, use the Payload class:
https://github.com/davestewart/vuex-pathify/blob/master/src/classes/Payload.js
https://davestewart.github.io/vuex-pathify/#/api/properties?id=payload-class
Something like this should work:
import { Payload } from 'vuex-pathify'
commit('SET_USER', new Payload('SET_USER', #profile.phoneNumber', phone_number);
Looks like I haven't documented this, so I have a made a ticket here:
https://github.com/davestewart/vuex-pathify/issues/80
It's using a commit from a component, but should work just the same.
People have asked before if it would be possible to use Pathify-style commits in actions and I said it wasn't, but I've just thought of something that might make it possible.
Follow this feature request for more info:
https://github.com/davestewart/vuex-pathify/issues/79

Where to setup an API in Vue.js

I need to use an API that requires initialization with an API key and some other details within my Vue.js app.
var client = api_name('app_id', 'api_key', ...)
I would need to make several API calls with the client object in multiple components in my app
client.api_function(...)
How can I avoid repeating the initialization step in every component?
I'm thinking about using a global mixin in main.js for that
Vue.mixin({
data: function() {
return {
get client() {
return api_name('app_id', 'api_key');
}
}
}
})
Is this a good approach?
I'd rather move your getter to a service and just import, where you actually need it. It doesn't seem to fit into data section, more like methods. A mixin is a decent approach if you need lots of similar stuff: variables, methods, hooks etc. Creating a mixin for only 1 method looks like overkill to me.
// helper.js
export function getClient () {
// do stuff
}
// MyComponent.vue
import { getClient } from 'helpers/helper`
// Vue instance
methods: {
getClient
}
How about creating a helper file and writing a plugin that exposes your api url's? You can then create prototypes on the vue instance. Here's an example,
const helper = install(Vue){
const VueInstance = vue
VueInstance.prototype.$login = `${baseURL}/login`
}
export default helper
This way you can access url's globally using this.$login. Please note $ is a convention to avoid naming conflicts and easy to remember that it is a plugin.

How to call a shared helper function from within a mutation or action in Vuex

I am trying to separate out some code that is common among many calls in my Vuex mutations. I am getting the feeling that this is discouraged but I don't understand why.
Have a look at an image of some sample code below:
I have added this 'helpers' entry in the Vuex - this obviously doesn't exist but how can I call the shared helper function 'getColumn' from mutations and/or actions?
Or do I have resort to calling a static method on a 'VuexHelper' class? :(
Something like:
Note
I have already looked at the following:
Vue Mixins - yes, something like that could work but is not
supported in Vuex - also, vue methods don't return a value...
I have looked at Modules but these still don't give me what I need, i.e. a simple re-usable function that returns a value.
Thanks
I don't see why you may want to put the helper function within the store. You can just use a plain function.
function getColumn(state, colName) {
// Do your thing.
}
const vstore = new Vuex.Store({
// ....
mutations: {
removeColumn(state, colName) {
var column = getColumns(state, colName);
}
}
};
On the other hand, if you really need that, you can access the raw module and all that's included:
var column = this._modules.root._rawModule.helpers.getColumns(state, colName);
Although this syntax is not documented and can change for later versions.
You can implement your Vuex getter as a method-style getter. This lets you pass in the specific column as an argument:
getters: {
getColumn: state => colName => {
return state.columns[colName] || null
}
}
Then getColumn can be used within the store like so:
let column = getters.getColumn('colNameString')
vuex docs > getters > method style access

Accessing getters within Vuex mutations

Within a Vuex store mutation, is it possible to access a getter? Consider the below example.
new Vuex.Store({
state: {
question: 'Is it possible to access getters within a Vuex mutation?'
},
mutations: {
askQuestion(state) {
// TODO: Get question from getter here
let question = '';
if (question) {
// ...
}
}
},
getters: {
getQuestion: (state) => {
return state.question;
}
}
});
Of course the example doesn't make much sense, because I could just access the question property directly on the state object within the mutation, but I hope you see what I am trying to do. That is, conditionally manipulating state.
Within the mutation, this is undefined and the state parameter gives access to the state object, and not the rest of the store.
The documentation on mutations doesn't mention anything about doing this.
My guess would be that it's not possible, unless I missed something? I guess the alternative would be to either perform this logic outside of the store (resulting in code duplication) or implementing an action that does this, because actions have access to the entire store context. I'm pretty sure that it's a better approach, that is to keep the mutation focused on what it's actually supposed to do; mutate the state. That's probably what I'll end up doing, but I'm just curious if accessing a getter within a mutation is even possible?
Vuex store mutation methods do not provide direct access to getters.
This is probably bad practice*, but you could pass a reference to getters when committing a mutation like so:
actions: {
fooAction({commit, getters}, data) {
commit('FOO_MUTATION', {data, getters})
}
},
mutations: {
FOO_MUTATION(state, {data, getters}) {
console.log(getters);
}
}
* It is best practice to keep mutations a simple as possible, only directly affecting the Vuex state. This makes it easier to reason about state changes without having to think about any side effects. Now, if you follow best practices for vuex getters, they should not have any side effects in the first place. But, any logic that needs to reference getters can run in an action, which has read access to getters and state. Then the output of that logic can be passed to the mutation. So, you can pass the reference to the getters object to the mutation, but there's no need to and it muddies the waters.
If you put your getters and mutations in separate modules you can import the getters in the mutations and then do this:
import getters from './getters'
askQuestion(state) {
const question = getters.getQuestion(state)
// etc
}
If anyone is looking for a simple solution, #bbugh wrote out a way to work around this here by just having both the mutations and getters use the same function:
function is_product_in_cart (state, product) {
return state.cart.products.indexOf(product) > -1
}
export default {
mutations: {
add_product_to_cart: function (state, product) {
if (is_product_in_cart(state, product)) {
return
}
state.cart.products.push(product)
}
},
getters: {
is_product_in_cart: (state) => (product) => is_product_in_cart(state, product)
}
}
You also can reference the Store object within a mutation, if you declare the store as an expression, like this:
const Store = new Vuex.Store({
state: {...},
getters: {...},
mutations: {
myMutation(state, payload) {
//use the getter
Store.getters.getter
}
}
});
Vuex 4
const state = () => ({
some_state: 1
})
const getters = {
some_getter: state => state.some_state + 1
}
const mutations = {
GET_GETTER(state){
return getters.some_getter(state)
}
}
Somewhere in you component
store.commit('GET_GETTER') // output: 2
Another solution is to import the existing store. Assuming you're using modules and your module is called foo, your mutator file would look like this store/foo/mutations.js:
import index from '../index'
export const askQuestion = (state, obj) => {
let store = index()
let getQuestion = store.getters['foo/getQuestion']
}
I'm not saying this is best practise but it seems to work.
At least in Vuex 3/4, I've reached for this pattern numerous times: simply use this.getters.
Inside a mutation, regardless if you are inside a Vuex module or not, this is always reference to your root store.
So:
mutations: {
doSomething(state, payload) {
if (this.getters.someGetter) // `this.getters` will access you root store getters
}
Just to be extra clear, this will work if the getter you're trying to access belongs to your root store OR to any other non-namespaced module, since they are all added (flattened) to this.getters.
If you need access to a getter that belongs to any namespaced Vuex module, just remember they are all added to the root store getters as well, you just have to use the correct key to get to them:
this.getters['MyModuleName/nameOfGetter']
The great thing about this is that it also allows you to invoke mutations that belong either to the root store or any other store. this.commit will work exactly as expected as well Source

Is there a way for a module to reference its own default export when it was assigned to an expression?

For example, in CommonJS:
var actions = module.exports = Flux.createActions({
SubmitSignup: function(payload) {
utils.xhr('POST', payload.url, payload.data, function(response, status){
actions.ReceiveSignupResponse({
response: response,
status: status,
receiver: payload.receiver
});
});
return payload;
},
ReceiveSignupResponse: function(payload) {
payload.receiver.handleSignupResponse(payload.response, payload.status);
return payload;
}
});
This exports the value of the Flux.createActions(...) expression as the module, and also assigns it to the local variable actions which is used internally so actions can refer to each-other. Even without the multi-assignment one-liner, the value of the expression would still be available as module.exports.
ES6 still allows for exporting a single value for the module using export default and even allows for assigning a local name to that value for classes and functions:
export default function foo() {...}
// or
export default class Foo { ...}
It also allows arbitrary expressions to be exported (export default (whatever());) and it allows local assignments to be exported as well (export let actions = Flux.createActions({...});), retaining the local name for local use, however, it does not allow exporting an assignment expression as a default - the following are all invalid:
export default let actions = Flux.createActions({...});
export default (let actions = Flux.createActions({...}));
export default as actions = Flux.createActions({...});
Nor is there a modules.exports equivalent that could be used to reference the value internally (in practice, tools like babel and other transpilers will let you mix-and match CommonJS and ES6 modules as you will, so modules.export will, in fact, work, but this is non-standard, almost accidental behavior).
It is of course still perfectly possible to simply declare the local first and export it later:
let actions = Flux.createActions({
SubmitSignup: function(payload) {
utils.xhr('POST', payload.url, payload.data, function(response, status){
actions.ReceiveSignupResponse({
response: response,
status: status,
receiver: payload.receiver
});
});
return payload;
},
ReceiveSignupResponse: function(payload) {
payload.receiver.handleSignupResponse(payload.response, payload.status);
return payload;
}
});
export default actions;
And that's fine. It just seems like a strange incongruity that you can export and assign local names for any non-default values, including arbitrary expressions, as well as default functions and classes, just not expressions.
Is there a better way of doing this that I'm missing?
Other than importing itself, I don't think there is.
The reason pretty much is that default is no valid identifier, so if you have you have to choose your own identifier anyway then you'll have to use the standard way of explicitly exporting:
let x = …;
export { x as y };
export default x;
Notice that export let x = … is only syntactic sugar for export { x as x }, i.e. where the local and exported name are the same, which for a default export never is the case.
Btw, default-exporting an assignment is indeed valid, you just can't use a variable declaration. Try
let actions;
export default (actions = Flux.createActions({...}));