[Vuejs]-For sibling communication between many identical components, how should I store the data in the lowest-common ancestor?

0👍

I ended up getting it working in a simple example in CodePen, which I’m going to use as a guide when trying to get it working on the actual site.

The summary of my findings with this solution is, "Vue will actually update when the nested entries of a Vuex state object are updated; you don’t need to worry about it not detecting those changes. So it’s OK to just keep all the data in a single big Vuex store object when you have many duplicate sibling components that need to react to each other."

Here’s the CodePen: https://codepen.io/NathanWailes/pen/NWRNgNz

Screenshot

enter image description here

Summary of what the CodePen example does

  • The data used to populate the menu all lives in the Vuex store in a single weeklyMenu object, which has child objects to break up the data into the different days / meals.
  • The individual meals have computed properties with get and set functions so that it can both get changes from the store and also update the store.
  • The DailyMenu and WeeklyMenu components get their aggregate data by simply having computed properties that iterate over the Vuex weeklyMenu object, and it "just works".
  • I have same-named meals update to match each other by iterating over the meals in the Vuex mutation and looking for meals with the same "Ingredient Name".

The code

HTML

<html>
  <body>
    <div id='weekly-menu'></div>
    <h3>Requirements:</h3>
    <ul>
      <li>Each row should have all the numbers in it summed and displayed ('total daily calories').</li>
      <li>The week as a whole should have all the numbers summed and displayed ('total weekly calories').</li>
      <li>If two or more input boxes have the same text, a change in one numerical input should propagate to the other same-named numerical inputs.</li>
      <li>Ideally the data (ingredient names and calories) should be stored in one place (the top-level component or a Vuex store) to make it more straightforward to populate it from the database with a single HTTP call (which is not simulated in this example).</li>
    </ul>
  </body>
</html>

JavaScript

const store = new Vuex.Store(
  {
    state: {
      weeklyMenu: {
        Sunday: {
          Breakfast: {
            name: 'aaa',
            calories: 1
          },
          Lunch: {
            name: 'bbb',
            calories: 2
          },
        },
        Monday: {
          Breakfast: {
            name: 'ccc',
            calories: 3
          },
          Lunch: {
            name: 'ddd',
            calories: 4
          },
        }
      }
    },
    mutations: {
      updateIngredientCalories (state, {dayOfTheWeekName, mealName, newCalorieValue}) {
        state.weeklyMenu[dayOfTheWeekName][mealName]['calories'] = newCalorieValue
        
        const ingredientNameBeingUpdated = state.weeklyMenu[dayOfTheWeekName][mealName]['name']
        for (const dayOfTheWeekName of Object.keys(state.weeklyMenu)) {
          for (const mealName of Object.keys(state.weeklyMenu[dayOfTheWeekName])) {
            const mealToCheck = state.weeklyMenu[dayOfTheWeekName][mealName]
            const ingredientNameToCheck = mealToCheck['name']
            if (ingredientNameToCheck === ingredientNameBeingUpdated) {
              mealToCheck['calories'] = newCalorieValue
            }
          }
        }
      },
      updateIngredientName (state, {dayOfTheWeekName, mealName, newValue}) {
        state.weeklyMenu[dayOfTheWeekName][mealName]['name'] = newValue
      }
    }
  }
)

var Meal = {
  template: `
    <td>
      <h4>{{ mealName }}</h4>
      Ingredient Name: <input v-model="ingredientName" /><br/>
      Calories: <input v-model.number="ingredientCalories" />
    </td>
  `,
  props:    [
    'dayOfTheWeekName',
    'mealName'
  ],
  computed: {
    ingredientCalories: {
      get () {
        return this.$store.state.weeklyMenu[this.dayOfTheWeekName][this.mealName]['calories']
      },
      set (value) {
        if (value === '' || value === undefined || value === null) {
          value = 0
        }
        this.$store.commit('updateIngredientCalories', {
          dayOfTheWeekName: this.dayOfTheWeekName,
          mealName: this.mealName,
          newCalorieValue: value
        })
      }
    },
    ingredientName: {
      get () {
        return this.$store.state.weeklyMenu[this.dayOfTheWeekName][this.mealName]['name']
      },
      set (value) {
        this.$store.commit('updateIngredientName', {
          dayOfTheWeekName: this.dayOfTheWeekName,
          mealName: this.mealName,
          newValue: value
        })
      }
    }
  }
};

var DailyMenu = {
  template: `
    <tr>
      <td>
        <h4>{{ dayOfTheWeekName }}</h4>
        Total Daily Calories: {{ totalDailyCalories }}
      </td>
      <meal :day-of-the-week-name="dayOfTheWeekName" meal-name="Breakfast" />
      <meal :day-of-the-week-name="dayOfTheWeekName" meal-name="Lunch" />
    </tr>
  `,
  props:    [
    'dayOfTheWeekName'
  ],
  data: function () {
    return {
    }
  },
  components: {
           meal: Meal
  },
  computed: {
    totalDailyCalories () {
      let totalDailyCalories = 0
      for (const mealName of Object.keys(this.$store.state.weeklyMenu[this.dayOfTheWeekName])) {
        totalDailyCalories += this.$store.state.weeklyMenu[this.dayOfTheWeekName][mealName]['calories']
      }
      return totalDailyCalories
    }
  }
};

var app = new Vue({ 
    el: '#weekly-menu',
  template: `<div id="weekly-menu" class="container">
    <div class="jumbotron">
    <h2>Weekly Menu</h2>
    Total Weekly Calories: {{ totalWeeklyCalories }}
    <table class="table">
        <tbody>
          <daily_menu day-of-the-week-name="Sunday" />
          <daily_menu day-of-the-week-name="Monday" />
        </tbody>
    </table>
    </div>
</div>
`,
  data: function () {
    return {
    }
  },
  computed: {
    totalWeeklyCalories () {
      let totalWeeklyCalories = 0
      for (const dayOfTheWeekName of Object.keys(this.$store.state.weeklyMenu)) {
        let totalDailyCalories = 0
        for (const mealName of Object.keys(this.$store.state.weeklyMenu[dayOfTheWeekName])) {
          totalDailyCalories += this.$store.state.weeklyMenu[dayOfTheWeekName][mealName]['calories']
        }
        totalWeeklyCalories += totalDailyCalories
      }
      return totalWeeklyCalories
    }
  },
  components: {
           daily_menu: DailyMenu
  },
  store: store
});

0👍

  1. Yes, problem domain seems complex enough to more than justify use of Vuex. I would not go with keeping data in components and sharing by props – that doesn’t scale well
  2. Keep each Recipe as an object in single object recipes – you don’t need to worry about watchers. If one particular Recipe object will change, Vue will re-render only components using same Recipe object (and if done properly you don’t even need watchers for that)
  3. Create a "weekly menu" object inside the store
  4. In leaf nodes (Meals) of that object just use some kind of reference (by name or unique ID if you have one) into recipes. As a result multiple Meal.vue components on a menu will use same object in the store and update automatically

Leave a comment