mmm-typed-vuex
v5.0.4
Published
Vuex and TypeScript living in harmony with one another.
Downloads
34
Maintainers
Readme
mmm-typed-vuex (beta 5)
Vuex and TypeScript living in harmony with one another 🎶
Installation
$ yarn add mmm-typed-vuex
$ npm install --save mmm-typed-vuex
Okay, but what is mmm-typed-vuex?
This library is all about:
- fewer string references in your Vue components (those suck)
- less guess work as to the structure of your store (thank goodness)
- typed state values, mutation / action payloads, and getters (it's about time)
- built-in init action for modules (why doesn't this exist by default?)
- simplify components even further by getting rid of mapState, mapActions, mapMutations, and mapGetters (optional)
- dispatch actions, get state, etc from anywhere in your code via a single-line command (easy-peasy like)
At its simplest, mmm-typed-vuex proposes something like this (yay typings!):
computed: {
count(): number { return AppStoreHelper.CounterStore.count; },
},
methods: {
increment(): void { AppStoreHelper.CounterStore.commitIncrement(2); },
}
Instead of this (boo!):
computed: {
...mapState({
count(state) { return state.CounterStore.count; }
}),
},
methods: {
...mapMutations('CounterStore', {
incrementMutation(commit): void { commit('increment', 3); }
}),
}
Why?
I could not be more sick of brittle string references! You know, those annoying set of characters that you copy and paste throughout your code. Yeah, we all know we shouldn't do it, but Vuex makes it especially tempting. Be honest, have you ever done something like this?
// App.vue
computed: {
...mapState({
title: 'title',
count(state) { return state.CounterStore.count; }
}),
...mapGetters('CounterStore', ['countX10'])
},
methods: {
...mapMutations('CounterStore', {
incrementMutation(commit): void { commit('increment', 3); }
}),
...mapActions({
decrementAction(dispatch): void { dispatch('CounterStore/decrement', 3); }
})
}
If so, you now have:
- static string references to a potentially complex and ever-changing store structure
- blind references to state properties
- very brittle code
Disgusting. Definitely not very 'mmm' ;)
What if one of your property or mutuation names change? What if you move, rename, or delete a module? Yep, you're going to have to find and update all those narly string references each time you make a change. Good luck if you're playing around with a substantial codebase.
Adding insult to injury, you sure as heck aren't getting typed state, mutation payloads, etc. Ouch!
Solution
Let the store, alone, define and strictly enforce its data through typings.
Enough talk, let me instead show you one possible alternative to the aforementioned string hell:
// App.vue
computed: {
// access state through the helper (though you can still use 'mapState' if you choose and still get typings)
title(): string { return AppStoreHelper.title; },
count(): number { return AppStoreHelper.CounterStore.count; },
// getters
countX10(): number { return AppStoreHelper.CounterStore.getCountX10(); }
},
methods: {
// convenience method that type-safes the mutation and payload
increment(): void { AppStoreHelper.CounterStore.commitIncrement(2); },
// convenience method that type-safes the action and payload
decrement(): void { AppStoreHelper.CounterStore.dispatchDecrement(2); }
}
Much better! It may appear a little verbose, but it's all typed and your editor's intelli-sense should be able to do all the heavy lifting. Oh...and no more string references!!
Okay, but what is mmm-typed-vuex really?
Honestly, it's not much...which was my main objective. We're talking about roughly 20 or so lines of meaningful code...but there is a dash of magic in there. It's just enough to avoid dealing with module paths, make a module's helper methods more accessible, and provide an init module action.
And now, for the measly sum of $0, all that magic can be yours ;)
Additional documentation
Vuex definition examples:
A quick note, this library attempts to be largely unopinionated and avoid recreating Vuex logic. This leaves the details of the store implementation and how your Vue component consumes it up to you. There's a few example provided show-casing different implementations. The following is just one example. Chances are, you'll find a better way to leverage this simple library...is you so choose :)
// CounterStore.module.ts
export default class CounterStore extends StoreModule {
// typings (the decorators approach is experimental and several other options can be found in the examples directory)
@mmmState public count: number;
@mmmMutation() public commitDecrement(payload: number) { return; }
@mmmMutation() public commitIncrement(payload: number) { return; }
@mmmAction() public dispatchDecrement(payload: number) { return; }
@mmmGetter() public getCountX10(): number { return NaN; }
constructor() {
super();
this.setOptions(
// this should be familiar...it's what you've already been doing except for (optionally) typing the state object
{
namespaced: true,
state: {
count: 0,
} as CounterStore, // optional typing
getters: {
getCountX10: (state: CounterStore): number => {
return state.count * 10;
},
},
mutations: {
commitDecrement(state: CounterStore, payload: number) {
state.count -= payload;
},
commitIncrement(state: CounterStore, payload: number) {
state.count += payload;
},
},
actions: {
initModule: (context: ActionContext<AppStore, AppStore>) => {
// finally, an easy way to asynchronously initialize state on module load, add 'this._store.watch', etc
},
dispatchDecrement: (context: ActionContext<CounterStore, AppStore>, payload: number) => {
this.commitDecrement(payload);
// dispatch to another module, in this case the root AppStore (wow is this easy!!!)
AppStoreHelper.dispatchChangeTitle('My New Title');
},
},
},
);
}
}
Additional Notes
- Those class properties (e.g.
public title: string;
) found within AppStoreHelper.ts are not -- in any way -- used to set state values (they're there only for typings) - Methods for mutations, actions, and getters simply type-safe payloads and simplify module paths
Available Examples
- A baseline, no typings example that showcases the brittleness of the typical string-heavy approach: baseline-no-typings
- A two-level deep 'module' example with concise, flattened module typing definitions: modules-flat-definitions
- An object-defined approach to typings that more clearly separates state, actions, mutations, getters and modules: modules-object-definitions
- An ES6 JavaScript example (no TypeScript): js-only
Potential negatives
Dynamic registration of modules can't leverage the provide init action...yet. It's coming!
There's more boilerplate. It sucks, but that's just the reality of it right now.
Rebuttal
On the flip-side, is the fact that once you write a typed vuex module, the rest of the app can effortlessly use it. No more banging your head against the wall trying to figure out the store structure or dealing with the fallout from refactoring all the string references scrattered throught your code. Is it worth the extra setup? That's for you to decide. For me, the answer is a resounding, "YES!". Once I'm done writing a store and am back to building out components and services, the last thing I want to do is to needlessly wrestle with the store. Plus, having the ability to leverage the init action is amazing!
Other ideas
This is surely not the best way this can be done, but it's at least a step in the right direction, I feel.
If you have ideas on how to improve upon this effort or want to contribute in any way, I'd definitely enjoy hearing from you! Two people's ideas are better than one.
Version 5 Notes
- The init action 'initMmm' has been renamed 'initModule'
- The store is now available from any modules via 'this._store'
Version 4 Notes
- Store modules are now set to the 'options' property and the root store and its modules must pass this property in during registration
- No more 'AppStore.init' in the store.ts
- The AppStoreHelper is new and makes calls more concise and removes an additional property lookup each call
- No more recursive lookups for module mutations and actions
- Init action is now available out of the box
Version 3 Notes
- Accessing state through 'RootScope' has been simplified and actions, mutations, and getters follow a similar format for consistency
- Defining module namespaces and parent modules is now done through the super constructor
Version 2 Notes
- The module helpers are no longer being stored in the store (hooray!)
- You no longer have to use mapState, mapActions, mapMutations, and mapGetters (of course, you can if you still want to)
- The method 'getModulePath' now caches the paths it caculates to avoid redundant processing on each request
Conclusion
That's it folks! Enjoy the Vue :)