Vuex

If you use Vue.js, then you probably use (or should consider using) Vuex for state management. Like most Vue.js related things, it’s at its easiest when used with Node tooling, but you can also use it with Apps Script HtmlService to manage state in  reactive add-ons.
I’ll assume that you are familiar with Vuex, so this article is largely about how to use it in the context of an htmlservice apps script add-on or webapp. Vuex is implemented in the template described in How to use Vue.js, Vuex and Vuetify to create Google Apps Script Add-ons

The cdn references

There’s 2 ways of including Vuex. One is by importing from npm as described in Including npm modules and Vue components: an htmlservice pattern for Apps Script add-ons, and another is to include the bundled version. We’ll go for the second of those options. Here’s the cdn.html (which also include vuetify and vue, which we’ll also need too)
<script src="https://cdn.jsdelivr.net/npm/babel-polyfill/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.5.1/vuex.min.js" integrity="sha512-n/iV5SyKXzLRbRczKU75fMgHO0A1DWJSWbK5llLNAqdcoxtUK3NfgfszYpjhvcEqS6nEXwu7gQ5bIkx6z8/lrA==" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.3.20/dist/vuetify.min.js"></script>
cdn.html

The Store

As usual, we want to keep the global space relatively uncluttered, so as per the template we’ll create the vuex store as a member of the Store namespace. The first job is to create the initial state of the store and define the getter, mutations and actions. The benefit of using Vuex is that it provides a focal point for all state changes and actions that modify state, and UI Vue components can automatically adapt rendering to the current state.

  // the initial contents of the vxStore
  const _initial = {
    state: {
      // whether polling is paused
      pollingPaused: false,
      // this is the latest package received from the server
      latestPackage: null,
      // this is the reconstructed current data
      // initially it just needs to know the sheetnames required
      currentPackage: {
        sheetNames: ["users"]
      },
      showThrow: false,
      throwMessage: null,
      pollQueue: null,
      simmer: null
    },
    getters: {
      simTime(state) {
        return state.simmer.time;
      },
      simRate(state) {
        return state.simmer.rate;
      },
      dataSets(state) {
        return state.currentPackage && state.currentPackage.everything;
      },
      users(state, getters) {
        return Packer.getData(getters.dataSets, "users");
      },
      getUser: (state, getters) => (userId) => {
        return getters.users.find((f) => f.id === userId);
      },
      transferring(state) {
        return state.pollQueue.activeSize() > 0;
      },
      dataChanged(state) {
        return state.currentPackage && state.currentPackage.changed;
      }
    },
    actions: {
      // can use this to start/restart the polling
      pause({ dispatch, commit, state }, value) {
        commit("setPollingPaused", value);
        commit("setTimerPaused", value);
        if (!value) {
          state.simmer.start();
          state.pollQueue.startQueue();
          dispatch("getData");
        } else {
          state.simmer.stop();
          state.pollQueue.stopQueue();
        }
      },
      stopEverything({ state }) {
        state.simmer.stop();
        state.pollQueue.stopQueue();
      },
      startSimmer({ state }) {
        state.simmer.start();
      },
      getData(sob) {
        return _poller(sob);
      }
    },
    mutations: {
      setSimmer(state, value) {
        state.simmer = value;
      },
      setSimRate(state, value) {
        state.simmer.rate = value;
      },
      setPollQueue(state, value) {
        state.pollQueue = value;
      },
      setPollingPaused(state, value) {
        state.pollingPaused = value;
      },
      // populate an error message
      setThrowMessage(state, value) {
        state.throwMessage = value;
      },
      // signal that an error should be shown
      setThrow(state, value) {
        state.showThrow = value;
      },
      // take the latest polling data and populate it with anything that hasnt changed
      setLatestPackage(state, value) {
        // this is the latest response from the server
        // if everything is null, then nothing has changed
        const { result, entry } = value;
        const { changed, cached } = result;

        state.latestPackage = result;

        state.currentPackage = Packer.reconstructEverything({
          current: state.currentPackage,
          latest: state.latestPackage
        });
        // only allow clearables on the first pass
        state.clearables = null;
      }
    }
  };
initialising Vuex

Mappers

Vuex provides some handy mappers that make it easy to include store defintions in Vue components. I’m taking this a bit further by automatically generating the mappers from the initial structure of the store.

  // handy for mapping in the components
  const _vxMaps = Object.keys(_initial).reduce((p, c) => {
    p[c] = Object.keys(_initial[c]);
    return p;
  }, {});

  ns.mapGetters = Vuex.mapGetters(_vxMaps.getters);
  ns.mapState = Vuex.mapState(_vxMaps.state);
  ns.mapMutations = Vuex.mapMutations(_vxMaps.mutations);
  ns.mapActions = Vuex.mapActions(_vxMaps.actions);
mapping

Now there’s argument that says that components should only map the state, getters, mutations and actions they plan to reference, but almost by definition, add-ons are going to be rather small so the convenience of mapping the entire store and including the mappings in each component is probably a fair tradeoff.

Using this approach anything from the Vuex store can easily be exposed in any component as easily as this

	computed: {
		...Store.mapGetters,
		...Store.MapState
	},
	methods: {
		...Store.mapMutations,
		...Store.mapActions
	}
	
including maps in Vue component

 

Initializing the store

We of course can’t initialize the store until we’ve imported Vue, Vuex and especially if we’re importing module  that might be a bit later, so I always like to include an .init method which contains things that can only be executed when all the scripts are imported and the Dom is settled. Here’s the init

  ns.init = ({ modules } = {}) => {
    // we're using Vuex extensively
    Vue.use(Vuex);

    // and this is the store
    ns.vxStore = new Vuex.Store(_initial);

    // set up things that needed some modules loaded asyncrnously,
    // which would have been done by now
    const { TimeSimmer, Qottle } = modules;
    const ms = TimeSimmer.ms;

    const simmer = new TimeSimmer({
      immediate: false,
      // update the simtime every 1 sec
      tickRate: ms("seconds", 1),
      // run in real time- set this to 1 - 60 means 1 minute passes in a second
      rate: 60,
      // we start now
      startedAt: new Date().getTime()
    });

    const qottle = new Qottle({
      // polling only 1 at a time, no more than 6 every minute
      // and with at least 7 secs between each one
      concurrent: 1,
      rateLimited: true,
      rateLimitPeriod: ms("minutes", 1),
      rateLimitMax: 6,
      rateLimitDelay: ms("seconds", 7)
    });
    ns.vxStore.commit("setSimmer", simmer);
    ns.vxStore.commit("setPollQueue", qottle);
    return ns;
  };
initializing the store

which is executed as part of the main initialization

window.onload =  () => {

  // need to wait for any modules to be imported
  const waitForModules = Store.moduleImports || Promise.resolve(null)

  waitForModules.then(modules=> {

    // this is an npm module but we can install just like the others
    Store.addComponent(modules.CountryFlag)
    
    // initialize everything and register all the components
    Store.init({modules})
      .components
      .registerAll()
    
    // render vue
    new Vue({
      el: '#app',
      vuetify: new Vuetify(),
      store: Store.vxStore
    })
    
    // start polling
    Store.load()

    /**
    * we need to detect when the add-on is closed to clean up
    */
    window.addEventListener("beforeunload", (event)=> {
      Store.unload()
      // Cancel the event as stated by the standard.
      event.preventDefault();
      // Older browsers supported custom message
      event.returnValue = '';
    });  

    /**
    * there's some action that could be taken when the tab becomes visible
    */
    Store.handleVisibility(new TabVisibility())
  })
main.js

Polling

Vuex and Qottle recipe: How to manage an asynchronous polling queue  make a useful combination to invisibly handle keeping the add-on view of the spreadsheet data in sync. Polling communicates with the Server for the latest view of the data, and transfers only changes to the sheet content. The data is then reconstructed. The reactivity of Vuex means that the Vue components will only be re-rendered if there’s been any changes in the spreadsheet, and all this can happen without caring about the details of how all that happens.

Indeed, this is all that’s required in a Vue component to know what’s the latest data in the users tab of a spreadsheet is

  computed: {
    items () {
      return this.users
    },
    ...Store.mapGetters
  }
bmSummary.vue

Summary

If you’re using the template from How to use Vue.js, Vuex and Vuetify to create Google Apps Script Add-ons  implementing Vuex in your add-on is rather straightforward (and highly recommended)