1

I'm trying to build a simple task management app with Vue and Electron. My setup is based on the electron-vue boilerplate with Vuex store. The user can add new items to the list (and edit existing items) through a modal. The modal sends the information to a store action which then calls a mutation to update the store and push a new item to the list item array.

This is the setup: LayerItem is a child of Layer which is a child of LayerMap. Data is received from the store within the parent LayerMap component and then provided to children via props.

Recreating the issue: Create a new item via the showEditItemDialog in Layer component. Within the SAVE_LAYER_ITEM mutation, a new ID will be created and assigned to that new item. After that, the new item will be pushed to the layer.items array. The UI will be updated and the created item is visible. item.text is displayed correct. The item.id however is different. I included a console.log within the mutation. The logged id doesn't match the id displayed in the UI within the LayerItem component here <p>{{ item.id }}</p>. As a result, when trying to edit/update a new item after it has been created, instead of updating the existing item, the mutation will create a new item since the ID received by the modal can't be found in the store array.

I know it's a lot of code, I tried to remove as much unnecessary code as possible. In the example below, I created a new item "test" and you can see that the stored ID doesn't match the ID displayed in the UI.

Screenshot from the Terminal logs enter image description here

Screenshot from the DevTools console enter image description here

Screenshot from Vue DevTools store enter image description here

Screenshot from the UI enter image description here

LayerMap.vue

// 'layers' is a computed property and gets data from the store
        <draggable
          v-model="layers"
          v-bind="getDragOptions"
        >
          <Layer v-for="(layer, index) in layers" :key="index" :layer="layer"></Layer>
        </draggable>
        <DetailsModal></DetailsModal>

// Inside computed
  computed: {
    layers() {
      return this.$store.getters.allLayers
    }
  }

Layer.vue

// 'layer' gets passed from parent as prop
     <span primary-focus @click="showEditItemDialog">Add Item</span> 
     <draggable v-model="items" v-bind="dragOptions" class="items">
        <LayerItem v-for="item in items" :item="item" :layer="layer" :key="item.id"></LayerItem>
      </draggable>

// 'items' is a computed property
    items: {
      get() {
        return this.layer.items
      }
    }

// Function to handle 'Add Item' click and send event which will be handled by DetailsModal.vue
  methods: {
    showEditItemDialog() {
      let payload = {
        layer: this.layer,
        item: {
          id: '',
          text: ''
        }
      }
      this.$bus.$emit('item-editing', payload)
    }
  }

LayerItem.vue

// Layer Item Component
  <div class="layer-item" @click.prevent="startEditing">
    <div class="item-body">
      <p>{{ this.item.text }}</p>
      <p>{{ item.id }}</p>
    </div>
  </div>

// Event will be sent on click with layer item details as parameter
  methods: {
    startEditing() {
      let payload = {
        layer: this.layer,
        item: {
          id: this.item.id,
          text: this.item.text
        }
      }
      this.$bus.$emit('item-editing', payload)
    }
  }
}

DetailsModal.vue

// 'editLayerForm' contains layer item id and text
      <p>{{editLayerForm.id}}</p>
      <div class="bx--form-item">
        <input
          type="text"
          v-model="editLayerForm.text"
        />
      </div>

// Inside <script>, event is received and handled, 'editLayerForm' will be updated with payload information
  mounted() {
    this.$bus.$on('item-editing', this.handleModalOpen)
  },
  methods: {
    handleModalOpen(payload) {
      this.layer = payload.layer
      this.editLayerForm.id = payload.item.id
      this.editLayerForm.text = payload.item.text
      this.visible = true
      console.log('editing', payload)
    },
    handleModalSave() {
      let payload = {
        layerId: this.layer.id,
        item: {
          id: this.editLayerForm.id,
          text: this.editLayerForm.text
        }
      }
      console.log('save', payload)
      this.$store.dispatch('saveLayerItem', payload)
    }
  }

Store.js

const actions = {
  saveLayerItem: ({ commit }, payload) => {
    console.log('action item id', payload.item.id)
    commit('SAVE_LAYER_ITEM', payload)
  }
}

const mutations = {
  SAVE_LAYER_ITEM: (state, payload) => {
    let layer = state.map.layers.find(l => l.id === payload.layerId)
    let itemIdx = layer.items.findIndex(item => item.id === payload.item.id)
    console.log('mutation item id', payload.item.id)

    if (itemIdx > -1) {
      // For existing item
      console.log('update item', payload.item)
      Vue.set(layer.items, itemIdx, payload.item)
    } else {
      // For new item
      payload.item.id = guid()
      console.log('save new item', payload.item)
      layer.items.push(payload.item)
    }
  }
}
Michal Levý
  • 33,064
  • 4
  • 68
  • 86
  • Too much code :) ...you are talking about your `Layer Item Component` displaying wrong `id` (btw what about text ?) but `item` is passed to the component through props. So the code which is using the component (and passing the item in) would be much more interesting (maybe your component is displaying completely different item?) – Michal Levý Nov 26 '19 at 14:51
  • @MichalLevý thanks for the response. I tried to update my question, remove unnecessary code and include parent components. Do you have any idea what is going wrong? – user2307798 Nov 26 '19 at 15:50
  • Not yet. `{{ this.item.text }}` in `LayerItem.vue` is real ? – Michal Levý Nov 26 '19 at 16:36
  • 1st step. Confirm the value of `id` in the store - is it original (logged) `id` or is it `id` as shown by your `LayerItem.vue` component? Use Vue Dev Tools... – Michal Levý Nov 26 '19 at 16:43
  • @MichalLevý again, thanks for trying to help! Really appreciated! Yes, `this.item.text` shows the correct value (I updated the text in my original question). Using the Vue Dev Tools, the store shows the value from `LayerItem.vue` and not the value logged by the store mutation. – user2307798 Nov 26 '19 at 16:55
  • My question about `{{ this.item.text }}` was badly formulated. You should not use `this` when referencing instance properties/data in the template. – Michal Levý Nov 26 '19 at 17:17
  • So the item in the store must have been changed in between the time it was inserted into store and was rendered. Did you tried [Vuex strict mode](https://vuex.vuejs.org/guide/strict.html#strict-mode)? It should warn you when data in the store are changed outside of mutations.... – Michal Levý Nov 26 '19 at 17:26
  • I do use Vuex strict mode, no warnings from that side. The weird thing is that the logs are completely different between Terminal and DevTools console. For example, the `console.log('mutation item id', payload.item.id)` shows empty in the Terminal (correct), but has a value in the DevTools console. The `console.log('save new item id', payload.item) then shows the exact value in the Terminal that has been shown in the DevTools console in the previous step. However it shows a new/different ID in the DevTools console. It seems like the mutation is called multiple times on the back/front end. – user2307798 Nov 26 '19 at 17:41
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/203130/discussion-between-michal-levy-and-user2307798). – Michal Levý Nov 26 '19 at 19:59

1 Answers1

2

Never did build Electron app before so it took me some time to dig deep enough but I think I got it! :)

Every electron app have at least 2 process - main (responsible for opening browser window) and renderer (where your Vue app runs). If you use console.log in your code, where the output shows depends on which process called it - console.log called from main process shows up only in the terminal window (used to start the app in dev mode), console.log called from renderer process shows up only in Dev Tools.

But logs from your mutations appear in both! Which means the code must be running in both processes, right ? But how?

Well it seems, electron-vue template has an option (you had to switch on when setting up the project) to use vuex-electron, particularly it's createSharedMutations plugin. It can be used to share same Vuex store between main process and all renderer processes (technically each process have its own store but state is synced). It works something like this:

  1. You fire your action (in renderer process)
  2. Action is canceled in renderer process (that's why you'll not see any logs from actions in Dev Tools) and notifies the main process to execute the action instead.
  3. Now if that action (running in main) commits a mutation, mutation code is executed in the main process (the 1st screenshot with logs from Terminal - id is empty) and then payload (now with newly generated id A) is serialized into JSON (see ipc-renderer) and passed to each renderer processes to execute same mutation (hence keeping all stores in sync). Here your mutation is executed 2nd time (2nd screenshot with logs from DevTools) - item has id already assigned (A) but it's not in the list of items so your code assigns new id (B) and push it into collection.
  4. id B is rendered on screen
  5. Now if you start editing and call action to save, everything described in point 3. will happen again but now mutation executing in main process sees item with id B which is not in its collection of items. So it assigns new id (C overwriting B) so mutation executing in renderer process again sees item with id C which is not in collection....and so on

Solution is obviously to disable createSharedMutations plugin in your store config (should be in /renderer/store/index.js). If you really need store synchronised across main process/renderer processes, you need to rewrite your mutations...

Michal Levý
  • 33,064
  • 4
  • 68
  • 86
  • Wow, thank you so much for putting so much effort into this! After reading this, I did some research and checked the setup of my vuex store. I removed both, the `createPersistedState()` as well as the `createSharedMutations()` plugin and added this line to `main.js`: `window.localStorage.clear()`. And now it's finally working :) Again, thank you for digging this deep. – user2307798 Nov 27 '19 at 23:23