1

(I am using Vuex but the question applies to any Flux architecture.)

I don't have a great understanding of the reasoning behind Flux architecture. I understand that it is nice to have all of the state in one place. I don't have a great understanding of other things though.

My question here is about where, exactly, the mutation logic should be. I get the impression that Flux architecture wants you to do have all of the mutation logic in one place - the store - because it gives you a good separation of concerns. Ie. when you want to know where mutation logic is, you know where to look.

But I find that it is nice to have classes and use instance methods to deal with state change. I think user.computeAndSetKarma() looks a lot nicer than storeHelper.computeAndSetKarmaForUser(user). Robert Martin talks about this in his book Clean Code:

A function with two arguments is harder to understand than a monadic function. For example, writeField(name) is easier to understand than writeField(output-Stream, name). Though the meaning of both is clear, the first glides past the eye, easily depositing its meaning. The second requires a short pause until we learn to ignore the first parameter. And that, of course, eventually results in problems because we should never ignore any part of code. The parts we ignore are where the bugs will hide.

...

Dyads aren’t evil, and you will certainly have to write them. However, you should be aware that they come at a cost and should take advantage of what mechanims may be available to you to convert them into monads. For example, you might make the writeField method a member of outputStream so that you can say outputStream.writeField(name). Or you might make the outputStream a member variable of the current class so that you don’t have to pass it. Or you might extract a new class like FieldWriter that takes the outputStream in its constructor and has a write method.

Consider this example (CodePen):

HTML

<div id="app">
  <table class="table">
     <thead>
       <tr>
         <th>Name</th>
         <th>Karma</th>
         <th>Compute karma (with logic done in store)</th>
         <th>Compute karma (with logic delegated to user)</th>
       </tr>
    </thead>
    <tbody>
      <tr v-for="(user, userIndex) in users">
        <td>{{ user.name }}</td>
        <td>{{ user.karma }}</td>
        <td>
          <button v-on:click="computeKarmaWithLogicDoneInStore(userIndex)">
            Compute
          </button>      
        </td>
        <td>
          <button v-on:click="computeKarmaWithLogicDelegatedToClass(user)">
            Compute
          </button>      
        </td>
      </tr>
    </tbody>
  </table>
</div>
<script src="https://cdn.jsdelivr.net/g/vue@2.0.3,vuex@2.0.0"></script>

JS

class User {
  constructor (name) {
    this.name = name;
    this.karma = 0;
    this.posts = [];
    this.comments = [];
  };

  computeAndSetNewKarma() {
    let postQualityScore = this._getPostQualityScore();
    let commentQualityScore = this._getCommentQualityScore();
    let activityScore = this._getActivityScore();
    let newKarma = this._getKarma(
      postQualityScore, 
      commentQualityScore, 
      activityScore
    );

    this.karma = newKarma;
  };

  _getPostQualityScore() {
    return 5;
  };
  _getCommentQualityScore() {
    return 10;
  };
  _getActivityScore() {
    return 15;
  };
  _getKarma(postQualityScore, commentQualityScore, activityScore) {
    return postQualityScore + commentQualityScore + activityScore;
  };
}

const storeHelper = {
  getPostQualityScore: function (posts) {
    return 5;
  },
  getCommentQualityScore: function (comments) {
    return 10;
  },
  getActivityScore: function (posts, comments) {
    return 15;
  },
  getKarma: function (postQualityScore, commentQualityScore, activityScore) {
    return postQualityScore + commentQualityScore + activityScore;
  },
};

const store = new Vuex.Store({
  state: {
    users: [
      new User('Adam'),
      new User('Bob'),
    ],
  },
  mutations: {
    computeKarmaWithLogicDoneInStore: function (state, userIndex) {
      let user = state.users[userIndex];
      let newPostQualityScore = storeHelper.getPostQualityScore(user.posts);
      let newCommentQualityScore = storeHelper.getCommentQualityScore(user.comments);
      let newActivityScore = storeHelper.getActivityScore(user.posts, user.comments);
      let newKarma = storeHelper.getKarma(
        newPostQualityScore, 
        newCommentQualityScore, 
        newActivityScore
      );

      user.karma = newKarma;
    },
    computeKarmaWithLogicDelegatedToClass: function (state, user) {
      user.computeAndSetNewKarma();
    },
  },
});

new Vue({
  store: store,
  el: '#app',
  computed: {
    users: function () {
      return this.$store.state.users;
    },
  },
  methods: {
    computeKarmaWithLogicDoneInStore: function (userIndex) {
      this.$store.commit('computeKarmaWithLogicDoneInStore', userIndex);
    },
    computeKarmaWithLogicDelegatedToClass: function (user) {
      this.$store.commit('computeKarmaWithLogicDelegatedToClass', user);
    },
  }
});

You could perform the logic inside of the mutation, or you could just call user.computeAndSetNewKarma() inside of the mutation, delegating the logic to the users instance method. The latter seems preferable to me, but I'm not sure if this violates convention, and I'm not sure if I am thinking about it properly.

  • As written, both of your approaches mutate the object in place, and so neither approach follows Vue's reactivity rules, right? (I haven't done Vue, but I have done React, and it looks like Vue follows a similar approach of discouraging in-place mutations.) – Josh Kelley Jun 15 '18 at 21:37
  • @JoshKelley Vue(x) does let you mutate the object in place while maintaining the reactivity. There are a few caveats to this though, and that's what the link was discussing. Eg. if you have let person = { name: 'John Doe' };, you can't do person.age = 30, you'd have to either initialize person.age = null upfront or do Vue.set(person, 'age', 30) if you want person to continue to be reactive. – Adam Zerner Jun 16 '18 at 16:19

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Browse other questions tagged or ask your own question.