Shared vuex state in a web-extension (dead object issues)
Asked Answered
M

2

6

I'm trying to use a shared vue.js state in a web extension.

The state is stored in the background script's DOM and rendered in a popup page.

First attempt

My first attempt was to use a simple store without vuex:

background.js

var store = {
  count: 0
};

popup.js

browser.runtime.getBackgroundPage().then(bg => {
    var store = bg.store;

    var vue = new Vue({
        el: '#app',
        data: {
            state: store
        },
    })
})

popup.html

<div id="app">
  <p>{{ state.count }}</p>
  <p>
    <button @click="state.count++">+</button>
  </p>
</div>
<script src="vue.js"></script>
<script src="popup.js"></script>

This works the first time the popup is opened (you can increment the counter and the value is updated) but when the popup is opened a second time, rendering fails with [Vue warn]: Error in render: "TypeError: can't access dead object". This seems to be caused by the fact that vue.js instance from the first popup modified the store by setting its own getter/setters, which have now been deleted since the first popup was closed, making the shared state unusable. This seems to be unavoidable, so I decided I'd give vuex a try.

Second attempt

background.js

var store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment: state => state.count++,
  }
})

popup.js

browser.runtime.getBackgroundPage().then(bg => {
    var store = bg.store;

    var vue = new Vue({
        el: '#app',
        computed: {
            count () {
                return store.state.count
            }
        },
        methods: {
            increment () {
                store.commit('increment')
            },
        }
    });
})

popup.html

<div id="app">
  <p>{{ count }}</p>
  <p>
    <button @click="increment">+</button>
  </p>
</div>
<script src="vue.js"></script>
<script src="popup.js"></script>

Unfortunately, this doesn't work either. You can open the popup and see the current counter value, but incrementing it doesn't update the view (you need to reopen the popup to see the new value).

When taking the same code but with the store declared in popup.js, the code works as intended so this should work, but for some reason it doesn't

My questions:

  • Is vue.js unable to handle this use case at all?
  • If so, would other frameworks (angular, react, ...) work here?
Milliary answered 17/3, 2018 at 20:44 Comment(0)
E
2

This does not work because your vue instance is not the same in background and popup. So, when you get the state from background, the watchers on the state makes react the Vue inside the background page, not the view in popup. You can achieve that by using the same store in background and popup and synchronizing the state between both. To synchronize the state, you can use the excellent plugin vuex-shared-mutations which uses localStorage to propagate mutations through different instances of a store. In your store, add

import createMutationsSharer from 'vuex-shared-mutations'
//...
export default new Vuex.Store({
   //...
   plugins: [createMutationsSharer({ predicate: ['increment'] })],
});

Now your popup is reacting to the button and your background is incremented. If you reopen the popup, count is 0 because you created a new store. You now need to load the initial state when the popup is initialized :

store.js :

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment: state => state.count++,
    setCount (state, count) {
      state.count = count
    },
  },
  plugins: [createMutationsSharer({ predicate: ['increment'] })],
  actions: {
    getCount ({ commit }) {
      browser.runtime.sendMessage({type: "storeinit", key: "count"}).then(count => {
        commit('setCount', count)
      })
    }
  }
});

background.js

import store from './store';
browser.runtime.onMessage.addListener((message, sender) => {
    if (message.type === 'storeinit') {
        return Promise.resolve(store.state[message.key]);
    }
});

popup.js

import store from '../store';
var vue = new Vue({
  //...
  created () {
    this.$store.dispatch('getCount')
  }
});

These difficulties are not vue related, react users have use a proxy to propagate state in browser extension : react-chrome-redux

Eldwon answered 25/3, 2018 at 17:7 Comment(0)
S
2

Sorry for the delay, I create a node module for it:

https://github.com/MitsuhaKitsune/vuex-webextensions

The module use the webextensions messaging API for sync all store instances on the webextension.

The install are like another vuex plugins, you can check it on the Readme.

If you have any question or feedback just tell me here or on github issues.

Silda answered 5/4, 2018 at 14:51 Comment(1)
Thank you for your contribution. I tested the example code on GitHub. It's working so well. But if it removed setInterval code in background.js, It does not work(could not sync between popup and content). Do you know why?Selfpossession

© 2022 - 2024 — McMap. All rights reserved.