vuejs / vuex

🗃️ Centralized State Management for Vue.js.
https://vuex.vuejs.org
MIT License
28.42k stars 9.57k forks source link

Enhancing Vuex modules #157

Closed piknik closed 8 years ago

piknik commented 8 years ago

I have this idea from #148 that I think would make programming large projects a ton easier, and maybe others would find it very useful. However, I'm not sure if it breaks the redux pattern itself and thus the point of Vuex, though it seems to fit in quite well. See how this could be used with some pseudocode below.

Introduction

Taken from #148, with proper modifications:

I believe the entire premise of the redux pattern is based on ES5 javascript, not that it's a bad thing, but ES5 doesn't have classes, so 'workarounds' like passing the state in parameters need to be done. Since it's so easy to make classes in es6 now, it seems kind of dated to me.

[A] "module" would look something like this maybe?

import * as Mutation from './mutations'

export default class CartStuff {
    constructor() {
        this.all = []
        this.status = "ok"
        this.unaccountedFee = 60
    }

    //Getter
    allItemsById() {
        return this.all.map(item => item.id)
    }
    //Getter
    itemsMappedProducts() {
        return this.all.map(item => this.state.products.all.find(product => product.id === item.productId))
    }

    //Action
    postItems(items) {
        this.dispatch(Mutation.CART_POST, items)

    }

    //Mutation
    [Mutation.ADD_TO_CART] (item) {
        this.all.push(item)
    }

    //Mutation
    [Mutation.CART_POST] (items) {
        this.status = 'posting'
        this.items = items
    }
}

Then in a Vue component, the module and its getters and actions can be referenced. Notice how the case of the class changes to camelCase:

<table>
    <tr v-for="item in cart.allItemsById">
        <td>item.name</td>
    </tr>
</table>
export default {
    //...
    vuex: {
        modules: ['cartStuff']
    },
    methods: {
        test() {
            this.cart.allItemsById
        }
    }
}

Global

Also, if you need some actions and getters that aren't relative to the module(s) you're targetting, simply do as you were with global getters and actions:

actions.js

export function actOnMultipleModules() {
    dispatch(WHATEVER, 10)
}
//...

Would be nice to have those automatic getters and setters, for these ones...

Bonus Outcomes

Some other uses I could think of, since we have a constructor for the modules we could do things like this in the future:


export default class MyModule {
    /**
    * constructor
    *
    * @param {function} autoStore A function that accepts a variable number of property names to automatically save and load
    */
    constructor(autoStore) {
        autoStore("items", "status")
        this.items = []
        this.status = "ok"
    }
    /...
}

This way Vuex handles loading and storing state automatically and theres not so much of this everywhere:

JSON.parse(localStorage.getItem("item")) || "default" 
JSON.parse(localStorage.getItem("bar")) || "foo" 

A different way

And if classes aren't anyone's thing, it could be done using object syntax:

export default {
    setup(autoStore) {
        autoStore("items", "status")
        return {
            items: [],
            status: "ok"
        }
    },
    actions: {
        //Same actions here
    },
    getters: {
        //Same getters here
    },
    mutations: {
        // Same mutations here
    }
}

Conclusion

Of course, all of this class stuff can be made optional, and the original redux-like syntax can be used alongside it.

If this is a good idea, I'd gladly implement this into Vuex myself after I finish my current project, which is within the week.

yyx990803 commented 8 years ago

I actively avoid classes in all Vue APIs, but the alternative object syntax looks interesting.

In fact, you can do something like this right now:

// in a module, cart.js

// the module passed to store constructors, same as before
export default {
  state: { ... }
  mutations: { ... }
}

// extra named export as a Vue component mixin
export const mixin = {
  getters: { ... },
  actions: { ... }
}

In component:

import { mixin as vuexCartMixin } from 'src/vuex/modules/cart'

export default {
  mixins: [vuexCartMixin]
}

One drawback I see with this is that it is no longer explicit in the component itself what properties and actions are exposed as a result of using Vuex. You'd have to checkout the module code to see what getters/actions are available. But it could indeed be much less verbose.

piknik commented 8 years ago

I understand the issue with classes.

Your use of mixins are interesting, something more like what can be implemented, though I don't believe it would work as it is; how does the store and state variables get put into the parameters of the action and getter functions? EDIT: Also, the getters and actions don't have some reference to the store/state internally, which would make parameters unneccesary, and a nice to have.

In my experience, I really wished there was a way to differentiate between different sets of getters by their module name, like cart.allItems and bag.allItems, instead I have to do allCartItems and allBagItems. Not that it's wrong, but I can analyze which module the getter comes from quicker. At the same time, it reduces polluting the component with potentially a lot of references to getters and actions.

yyx990803 commented 8 years ago

@piknik that's a good point - I think allowing component vuex option to take in modules is probably a good idea, but I would avoid using string ids:

import cart from 'src/vuex/modules/cart'

export default {
  vuex: {
    modules: {
      cart
    }
  }
}

When you import actions/getters via a module, they get auto-nested under the name of that module, so you get this.cart.doStuff().

pdcmoreira commented 8 years ago

@yyx990803 so in that case, what structure should the cart have in order to do this.cart.getItems() ?

I'm struggling to organize my medium-large sized app, I need to call something like this.products.getAll(), this.cart.getAllProducts() (or this.products.all), that separation by modules in the calling component seems ver good though.

Also, keep in mind that actions should not be per store because an action can call shared mutations. I would say that actions should be per "service" or something like that.

rtibbles commented 8 years ago

This may not be completely on topic - but I see the benefit of being able to import at least getters via the module interface. If you have the mutations and the state namespaced via the module object, then it makes sense to have getters that are similarly namespaced, so that you do not have to redundantly specify the namespace within those getters (and in fact, you can reusably import those getters into different modules when you have common getter behaviour across different modules).

yyx990803 commented 8 years ago

Please see #236