-1

I have a chrome extension with the following webpack.config.js:

module.exports = {
  mode,
  entry: {
    "content/content": [
      "./src/js/content/content.js",
      "./src/js/store.js",
      "./src/js/content/overlay/style.scss",
    ],
    "background/background": [
      "./src/js/background/utils.js",
      "./src/js/background/background.js",
    ],
    "overlay/overlay": "./src/js/content/overlay/index.js",
    "popup/popup": "./src/js/content/popup/index.js",
  },

looking at

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

https://github.com/xanf/vuex-shared-mutations

Adding a wrapper around browser local storage:

browserStore.js

import browser from "@/js/browser";

export function getStorageValue(payload) {
  return new Promise((resolve) => {
    browser.storage.local.get(payload, (items) => {
      if (items) {
        resolve(items);
      }
    });
  });
}

export function setStorageValue(payload) {
  return new Promise((resolve) => {
    browser.storage.local.set(payload, (value) => {
      resolve(value);
    });
  });
}

In "./src/js/content/popup/firstpage/store/index.js" vuex store is defined as:

import Vue from "vue";
import Vuex from "vuex";
import "es6-promise/auto";
import createMutationsSharer from "vuex-shared-mutations";

import dummyData from "./dummyData";

import { getStorageValue, setStorageValue } from "@/js/store";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    chromePagesState: {
      allSections: [],
    },
  },
  getters: {
    ...
  },
  mutations: {
    setChromePagesState(state, value) {
        ...
    },
    // this function is to be called from a content script
    addWhiteListedItem(state, item) {
      // state not initialized here
      state.chromePagesState.allSections[0].itemSectionCategory[0].tasks.splice(
        0,
        0,
        item
      );
    },
    ...
  }
  actions: {
    async saveChromePagesState({ state }) {
      // Save only needed fields
      let data = {
        ...
      };
      await setStorageValue({ inventoryData: JSON.stringify(data) });
    },
    async loadChromePagesState({ commit }) {
      const json = await getStorageValue("inventoryData");
      // json always an empty object 
      commit(
        "setChromePagesState",
        Object.keys(json).length === 0 && json.constructor === Object
          ? json
          : dummyData
      );
    },
    async loadChromePagesStateBrowser({ commit }) {
      browser.runtime
        .sendMessage({ type: "storeinit", key: "chromePagesState" })
        .then(async (chromePagesState) => {
          const json = await getStorageValue("inventoryData");
          commit(
            "setChromePagesState",
            Object.keys(json).length === 0 && json.constructor === Object
              ? json
              : dummyData
          );
        });
    },
    plugins: [
        createMutationsSharer({
          predicate: [
            "addWhiteListedItem",
            "loadChromePagesState",
            "loadChromePagesStateBrowser",
          ],
        }),
    ],
  },

the background script has a listener; src/background/background.js:

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

The content script that needs to make use of the shared store has an entry point in content.js:

import { initOverlay } from '@/js/content/overlay';
import browser from '@/js/browser';

browser.runtime.onMessage.addListener(function (request, _sender, _callback) {
  // vue component gets created here:
  if (request && request.action === 'show_overlay') {
    initOverlay();
  }

  return true; // async response
});

initOverlay() creates a vue component in ./src/js/content/overlay/index.js:

import Vue from "vue";
import Overlay from "@/js/content/overlay/Overlay.vue";
import browser from "@/js/browser";
import { getStorageValue } from "@/js/store";

import store from "../popup/firstpage/store";

Vue.prototype.$browser = browser;

export async function initOverlay(lockScreen = defaultScreen, isPopUp = false) {
    ...
    setVueOverlay(overlayContainer, cover);
    ...
}

  function setVueOverlay(overlayContainer, elem) {
    if (!elem.querySelector("button")) {
      elem.appendChild(overlayContainer);
      elem.classList.add("locked");

      new Vue({
        el: overlayContainer,
        store,
        render: (h) => h(Overlay, { props: { isPopUp: isPopUp } }),
      });
    }
  }

Overlay.vue only needs to call a mutation (addWhiteListedItem) from store:

<template>
              <button
                @click="addToWhiteList()"
                >White list!</button
              >
</template>

<script>
import { mapState, mapMutations } from "vuex";

export default {

  data() {
    return {
    };
  },
  computed: mapState(["chromePagesState"]),
  methods: {
    ...mapMutations(["addWhiteListedItem"]),
    addToWhiteList() {
      console.log("addToWhiteList()");

        let newItem = {
           ... 
        };
        // store not defined fails with:
        Uncaught TypeError: Cannot read property 'itemSectionCategory' of undefined
        at Store.addWhiteListedItem (index.js:79)
        at wrappedMutationHandler (vuex.esm.js:853)
        at commitIterator (vuex.esm.js:475)
        at Array.forEach (<anonymous>)
        at eval (vuex.esm.js:474)
        at Store._withCommit (vuex.esm.js:633)
        at Store.commit (vuex.esm.js:473)
        at Store.boundCommit [as commit] (vuex.esm.js:418)
        at VueComponent.mappedMutation (vuex.esm.js:1004)
        at eval (Overlay.vue?./node_modules/vue-loader/lib??vue-loader-options:95)
        this.addWhiteListedItem(newItem);

      }, 1500);
    },
  },
};
</script>

Why doesn't Overlay.vue "see" the state of store?

Flow:

  • enabling the extension injects a content script into a page
  • content script imports store object (that is not yet initialized)
  • upon clicking popup (/new tab) popup.js sends a message to the background script that also imports store and calls a mutation (that initializes state):

background.js

import store from "../content/popup/firstpage/store";
browser.runtime.onMessage.addListener((message, sender) => {
  console.log("in background");
  if (message.type === "storeinit") {
    console.log("got storeinit message. Message key: ", message.key);
    store.dispatch("loadChromePagesState");
    console.log("current state in store:", JSON.stringify(store.state));
    console.log(
      "store.state[message.key]:",
      JSON.stringify(store.state[message.key])
    );
    return Promise.resolve(store.state[message.key]);
  }
});
  • now the store's state should be initialized and the mutation callable from the content script (vue-shared-mutations guarantees it)

Does export default new Vuex.Store mean that every script that imports the store gets a new instance with a default state that is not in sync with other imports?

Sebi
  • 4,262
  • 13
  • 60
  • 116
  • There's no mention of allSections in the code. If you don't set it, it cannot have an object as element 0. – Estus Flask Aug 18 '21 at 08:17
  • @EstusFlask Updated question; allSections is initialized in store but the initialization is triggered in a background script. – Sebi Aug 24 '21 at 20:11

1 Answers1

0

As the error message suggests itemSectionCategory can not be found as it is expected to be an element of allSections[0]. However you never define index 0 of allSections before calling it.

So in short you need to either define allSections index 0 before using it, or make the index part optional and create it if it's not found.


Otherwise you could try one of the following solutions:

  1. if you need to rely on index 0 being available, check if it is set before calling your function !state.chromePagesState.allSections[0] ? [... insert initialize function call ...]

  2. Maybe optional chaining could be another solution depending on what you use it for afterwards, for an example How to use optional chaining with array or functions?

onewaveadrian
  • 434
  • 1
  • 6
  • 17
  • That's not the case; the store should be initialized by that time. Flow: background script handles store initialization and is triggered by a popup script. The background script calls a mutation from store which should normally initialize it's state (vuex-shared-mutations should handle the sync between scripts). Editing question. – Sebi Aug 24 '21 at 19:40
  • I'm not sure does `export default new Vuex.Store` create a new store object for every import of store? – Sebi Aug 24 '21 at 19:42
  • Since you didn't explain what `setChromePagesState(state, value) {}` does it's hard to tell. Can you set up a minimal reproducible example on codepen or right here on SO? – onewaveadrian Aug 24 '21 at 21:48
  • Updated my answer with two more ways – onewaveadrian Aug 24 '21 at 21:50
  • The problem is that the store object is not the same in the content script as in the background/popup script; even if initialized correctly in content it will be out of sync with background; actually entirely separate. – Sebi Aug 25 '21 at 20:09