Vue 3: Wait until parent is done with data fetching to fetch child data and show loader
Asked Answered
C

2

8

I'm looking for a reusable way to display a full page loader (Sidebar always visible but the loader should cover the content part of the page) till all necessary api fetches has been done.

I've got a parent component LaunchDetails wrapped in a PageLoader component

LaunchDetails.vue

<template>
  <PageLoader>
    <router-link :to="{ name: 'launches' }"> Back to launches </router-link>
    <h1>{{ name }}</h1>

    <section>
      <TabMenu :links="menuLinks" />
    </section>

    <section>
      <router-view />
    </section>
  </PageLoader>
</template>

<script>
import TabMenu from "@/components/general/TabMenu";

export default {
  data() {
    return {
      menuLinks: [
        { to: { name: "launchOverview" }, display_name: "Overview" },
        { to: { name: "launchRocket" }, display_name: "Rocket" },
      ],
    };
  },
  components: {
    TabMenu,
  },
  created() {
    this.$store.dispatch("launches/fetchLaunch", this.$route.params.launch_id);
  },
  computed: {
    name() {
      return this.$store.getters["launches/name"];
    },
  },
};
</script>

PageLoader.vue

<template>
  <Spinner v-if="isLoading" full size="medium" />
  <slot v-else></slot>
</template>

<script>
import Spinner from "@/components/general/Spinner.vue";

export default {
  components: {
    Spinner,
  },
  computed: {
    isLoading() {
      return this.$store.getters["loader/isLoading"];
    },
  },
};
</script>

The LaunchDetails template has another router-view. In these child pages new fetch requests are made based on data from the LaunchDetails requests.

RocketDetails.vue

<template>
  <PageLoader>
    <h2>Launch rocket details</h2>

    <RocketCard v-if="rocket" :rocket="rocket" />
  </PageLoader>
</template>

<script>
import LaunchService from "@/services/LaunchService";
import RocketCard from "@/components/rocket/RocketCard.vue";

export default {
  components: {
    RocketCard,
  },
  mounted() {
    this.loadRocket();
  },
  data() {
    return {
      rocket: null,
    };
  },
  methods: {
    async loadRocket() {
      const rocket_id = this.$store.getters["launches/getRocketId"];

      if (rocket_id) {
        const response = await LaunchService.getRocket(rocket_id);
        this.rocket = response.data;
      }
    },
  },
};
</script>

What I need is a way to fetch data in the parent component (LaunchDetails). If this data is stored in the vuex store, the child component (LaunchRocket) is getting the necessary store data and executes the fetch requests. While this is done I would like to have a full page loader or a full page loader while the parent component is loading and a loader containing the nested canvas.

At this point the vuex store is keeping track of an isLoading property, handled with axios interceptors.

All code is visible in this sandbox

(Note: In this example I could get the rocket_id from the url but this will not be the case in my project so I'm really looking for a way to get this data from the vuex store)

Celom answered 16/12, 2021 at 9:14 Comment(0)
E
4

Im introduce your savior Suspense, this feature has been added in vue v3 but still is an experimental feature. Basically how its work you create one suspense in parent component and you can show a loading when all component in any depth of your application is resolved. Note that your components should be an async component means that it should either lazily loaded or made your setup function (composition api) an async function so it will return an async component, with this way you can fetch you data in child component and in parent show a fallback if necessary.

More info: https://vuejs.org/guide/built-ins/suspense.html#suspense

Encompass answered 21/12, 2021 at 11:26 Comment(0)
S
2

You could use Events:

var Child = Vue.component('child', {
  data() {
    return {
      isLoading: true
    }
  },
  template: `<div>
    <span v-if="isLoading">Loading &hellip;</span>
    <span v-else>Child</span>
  </div>`,
  created() {
    this.$parent.$on('loaded', this.setLoaded);
  },
  methods: {
    setLoaded() {
      this.isLoading = false
    }
  }
});

var Parent = Vue.component('parent', {
  components: { Child },
  data() {
    return {
      isLoading: true
    }
  },
  template: `<div>
    Parent
    <Child />
  </div>`,
  mounted() {
    let request1 = new Promise((resolve, reject) => {
      setTimeout(resolve, 1000);
    });
    let request2 = new Promise((resolve, reject) => {
      setTimeout(resolve, 2000);
    });
    Promise.all([ request1, request2 ]).then(() => this.$emit('loaded'))
  }
});

new Vue({
  components: { Parent },
  el: '#app',
  template: `<Parent />`
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

This may be considered an anti-pattern since it couples the parent with the child and events are considered to be sent the other way round. If you don't want to use events for that, a watched property works just fine, too. The non-parent-child event emitting was removed in Vue 3 but can be implemented using external libraries.

Swamp answered 23/12, 2021 at 9:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.