Search code examples
typescriptvue.jspinia

VueJS Race condition during setup methods


I have been experimenting with VueJS and have run into a pretty frustrating issue dealing with promises and component state. I come from React so I could be going about this incorrectly. I have been unable to work my around around this particular issue.

I am using Pinia to store the state and I am trying to update a CharacterSummary object within the Home component's setup method. However I am hitting a race condition with a request that needs to happen in the top level App component.

My question is what is the proper way to handle an "initial loading" scenario using global state in vue?

Here is the store definition. All api calls are async and generated using the openapi-typescript-codegen package

import {
  Api,
  type WowCharacterSummary,
  type WowPlayerCharacter,
} from "@/client";
import { defineStore } from "pinia";

interface PlayerStore {
  playerCharacters: WowPlayerCharacter[];
  currentSelectedCharacter: string;
  characterSummary: WowCharacterSummary | null;
}

export const usePlayerStore = defineStore("player", {
  state: (): PlayerStore => ({
    playerCharacters: [],
    currentSelectedCharacter: "",
    characterSummary: null,
  }),
  actions: {
    async loadInitial() {
      const api = new Api();

      const access_token = "";

      if (access_token) {
        const playerCharactersResponse =
          await api.default.getPlayerUserInfoPlayerInfoGet(access_token);

        this.playerCharacters = playerCharactersResponse;
      }
    },
    async loadCharacterInfo(charName: string) {
      const api = new Api();

      const charResult =
        await api.default.getCharacterSummaryCharacterNameSummaryGet(charName);

      this.characterSummary = charResult;
    },
  },
});

In App.vue I am calling the loadInitial method to populate the playerCharacters state. It should be noted that if I await this call, the whole app stops loading. Adding <Suspense></Suspense> at the top level doesn't seem to help.

<script setup lang="ts">
import { RouterView } from "vue-router";
import BaseLine from "./components/layouts/BaseLine.vue";
import { usePlayerStore } from "./stores/player";

const { loadInitial } = usePlayerStore();
loadInitial();
</script>

<template>
  <BaseLine>
    <Suspense>
      <RouterView />
    </Suspense>
  </BaseLine>
</template>

And in HomeView.vue I am trying to use the values within playerCharacters to call a second endpoint and load more information.

<template>
  <main>
    <ul>
      <li v-for="c in playerCharacters" :key="c.name">
        <p>{{ c.name }}</p>
      </li>
    </ul>
    <div>
      <h1>{{ characterSummary?.name }}</h1>
      <h2>{{ characterSummary?.level }}</h2>
    </div>
  </main>
</template>

<script lang="ts">
import { usePlayerStore } from "@/stores/player";
import { storeToRefs } from "pinia";

export default {
  setup: async () => {
    const playerStore = usePlayerStore();
    const { characterSummary, playerCharacters } = storeToRefs(playerStore);

    if (playerCharacters.value.length > 0) {
      await playerStore.loadCharacterInfo(playerCharacters.value[0].name);
    }

    return { playerCharacters, characterSummary };
  },
};
</script>

The list of character names loads correctly every time but the charcaterSummary will only display if I have a break point in the right place, so it is very clearly a race condition issue.

Any help would be appreciated, I just need some help seeing what I am doing wrong.


Solution

  • loadInitial isn't waited during initial render, this results in race condition.

    Asynchronous components with async setup should be wrapped with <suspense>, they cannot work otherwise because setup function returns promise object instead of component instance and needs to be treated in a special way by Vue renderer.

    In case top-level component is asynchronous, it needs to be wrapped with another component that contains <suspense>.

    Alternatively, the application can be initialized prior to rendering:

    await loadInitial();
    app.mount(...);