Waiting for Vuex actions in Nuxt layouts

July 16th 2021 Vuex NuxtJS

Thanks to Vue's reactivity, it usually does not matter where and when Vuex state is initialized. State can be exposed in components as computed properties, and the page is automatically updated when their values change.

This works great in Nuxt, even when Vuex state is needed in both layout and page. Vuex state can be initialized in the layout:

import Vue from "vue";
import Component from "vue-class-component";
import { getFakeModule } from "~/store";

@Component({
  async mounted(this: DefaultLayout) {
    // initialize store
    await getFakeModule(this.$store).getState();
  },
})
export default class DefaultLayout extends Vue {
  get timestamp(): number {
    return getFakeModule(this.$store).timestamp;
  }

  get isStoreInitialized(): boolean {
    return getFakeModule(this.$store).isInitialized;
  }
}

And it can be used without initialization in all pages based on that layout:

import Vue from "vue";
import Component from "vue-class-component";
import { getFakeModule } from "~/store";

@Component
export default class DeclarativePage extends Vue {
  get timestamp(): number {
    return getFakeModule(this.$store).timestamp;
  }

  get isStoreInitialized(): boolean {
    return getFakeModule(this.$store).isInitialized;
  }
}

Although the page is rendered before initialization is complete, the values of the computed properties are automatically updated when the state is finally initialized:

<template>
  <div>
    <div v-if="isStoreInitialized">Page timestamp: {{ timestamp }}</div>
  </div>
</template>

However, if a value from the state is used programmatically on the page (for example, to call an API from the mounted hook), Vue reactivity cannot be relied upon to trigger that code when the Vuex state is initialized and the value is ready.

import Vue from "vue";
import Component from "vue-class-component";
import { getFakeModule } from "~/store";

@Component({
  async mounted(this: ImperativeActionPage) {
    const fakeModule = getFakeModule(this.$store);

    const timestamp = fakeModule.timestamp;

    // use value as a parameter to a method call
  },
})
export default class ImperativeActionPage extends Vue {}

Such code will still work fine most of the time. Nuxt applications are SPAs and if the user has navigated to such a page from another page in the application, the initialization of the layout will have long since completed by the time the initialization of the new page begins.

The problem only occurs when the user navigates to that page from outside the application, or when the user refreshes the page in the browser. Then the layout must also be initialized and the Vuex state is not reliably ready when used in the page initialization.

One way to solve this problem would be to initialize the Vuex state from the page if it is not ready at the time its data is needed:

import Vue from "vue";
import Component from "vue-class-component";
import { getFakeModule } from "~/store";

@Component({
  async mounted(this: ImperativeActionPage) {
    const fakeModule = getFakeModule(this.$store);

    if (!this.isStoreInitialized) {
      await fakeModule.getState();
    }

    const timestamp = fakeModule.timestamp;

    // use value as a parameter to a method call
  },
})
export default class ImperativeActionPage extends Vue {
  get isStoreInitialized(): boolean {
    return getFakeModule(this.$store).isInitialized;
  }
}

This will work. But it will have the side effect of initializing the Vuex state twice, if the initialization in the layout is not yet complete when the Vuex state is read from the page. This is inefficient (e.g. because of redundant API calls to initialize the state).

A better solution is to wait until initialization is complete before attempting to access the state. The Vuex API allows you to do just that. The subscribe function can be used to be notified each time a mutation is committed (after the state has already been updated). This is a good solution since every initialization action ends with the commit of a mutation to set the initial state.

To use the function with async and await, it can be wrapped in a promise:

export function waitForMutation(
  store: Store<any>,
  mutationType: string
): Promise<void> {
  return new Promise((resolve) => {
    const unsubscribe = store.subscribe((mutation, _state) => {
      if (mutation.type === mutationType) {
        unsubscribe(); // stop receiving notifications
        resolve();
      }
    });
  });
}

The page can now wait for the initialization action to complete instead of triggering it again:

import Vue from "vue";
import Component from "vue-class-component";
import { getFakeModule, waitForMutation } from "~/store";

@Component({
  async mounted(this: ImperativeActionPage) {
    const fakeModule = getFakeModule(this.$store);

    if (!this.isStoreInitialized) {
      await waitForMutation(this.$store, "fake/setState");
    }

    const timestamp = fakeModule.timestamp;

    // use value as a parameter to a method call
  },
})
export default class ImperativeActionPage extends Vue {
  get isStoreInitialized(): boolean {
    return getFakeModule(this.$store).isInitialized;
  }
}

You can check out the sample application from my GitHub repository. It contains three pages, each showing one of the above scenarios:

  • Exploiting Vue reactivity to use the value declaratively in the page.
  • Invoking initialization from the page if the state is not yet ready when needed.
  • Waiting for the initialization to complete in the layout if the state is not ready when the page needs it.

Vue's reactivity allows you to not worry about initializing Vuex state most of the time. The page will simply update when the (new) data is ready. If you need to react programmatically to data changes by calling an API, you can do that too. The Vuex subscribe method allows you to be notified when a mutation is committed.

However, take a look at the rest of the Vuex API. One of the other functions might be a better fit for your use case.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License