Search code examples
vue.jsvuejs3vue-routervue-composition-apivuejs-slots

How to prevent view re-rendering inside dynamic layout when route (and therefore dynamic layout) is changed in Vue 3?


When I navigate from the page from MainLayout to the page that has CallLayout, Call view is re-rendered.

AppLayout.vue:

<script lang="ts" setup>
import { markRaw, shallowRef, watch } from 'vue';
import { useRoute } from 'vue-router';
import DefaultLayout from '@/layouts/DefaultLayout.vue';

const layout = shallowRef();
const route = useRoute();

watch(layout, () => console.log('%c Layout has been changed', 'color: aqua'));

watch(
  () => route.meta?.layout,
  async (metaLayout) => {
    console.log('%c Updating layout', 'color: aqua');
    try {
      const component =
        metaLayout && (await import(/* @vite-ignore */ `./${metaLayout}.vue`));
      layout.value = markRaw(component?.default || DefaultLayout);
    } catch (error) {
      layout.value = markRaw(DefaultLayout);
    }
  }
);
</script>

<template>
  <Component :is="layout">
    <slot />
  </Component>
</template>

Call.vue:

<script lang="ts" setup>
console.log('Render Call component');
</script>

<template>
  <p>Call component</p>
</template>

I have the following logs:

logs


Solution

  • After some time I figured out the cause of the problem. Since I import component asynchronously, watch callback is also asynchronous. However, vue doesn't wait for callback completion and renders layout and the view immediately after callback call. When component is imported, layout gets updated and vue re-renders the layout along with the view.

    We cannot make vue wait for asynchronous callbacks, but we can disable rendering till layout gets imported by introducing another reactive variable layoutLoading and using conditional rendering. Here is fixed version of AppLayout.vue:

    <script lang="ts" setup>
    import { markRaw, ref, shallowRef, watch } from 'vue';
    import { useRoute } from 'vue-router';
    import DefaultLayout from '@/layouts/DefaultLayout.vue';
    import Loading from '@/components/Shared/Loading.vue';
    
    const layout = shallowRef();
    const route = useRoute();
    
    const layoutLoading = ref(true);
    
    watch(
      () => route.meta?.layout,
      async (metaLayout) => {
        layoutLoading.value = true;
    
        try {
          const component =
            metaLayout && (await import(/* @vite-ignore */ `./${metaLayout}.vue`));
    
          layout.value = markRaw(component?.default || DefaultLayout);
        } catch (error) {
          layout.value = markRaw(DefaultLayout);
        }
    
        layoutLoading.value = false;
      }
    );
    </script>
    
    <template>
      <Component :is="layout" v-if="!layoutLoading">
        <slot />
      </Component>
      <Loading v-else></Loading>
    </template>