user2307798
user2307798

Reputation: 67

Strange behavior of Vuex mutations in Electron app based on electron-vue

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)
    }
  }
}

Upvotes: 1

Views: 452

Answers (1)

Michal Lev&#253;
Michal Lev&#253;

Reputation: 37913

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...

Upvotes: 2

Related Questions