Search code examples
typescriptvuejs3refvue-script-setupvue-suspense

Vue3/Vite Typescript: ref is undefined when used on element inside <Suspense #default>


I'm working on a Vue3/Vite Typescript project using <Suspense> to show a loading state template #fallback while async child components are being loaded. This works like a charm.

However I would like to have a ref of one/multiple elements inside my <Suspense #default> DOM structure (e.g. adding an IntersectionObserver for some nice animations).

I understand that the "target" ref is still undefined when the onMounted() hook is triggered, since this happens when all static content is ready (while the async components are still loading; the "loader" ref returns the <div class="loader">). <Suspense> provides the onResolve() hook, which is triggered after all contained async components in the template #default are loaded. Still the "target" ref is undefined (there the "loader" ref returns null - which is ok since it's not shown any more).

Test: If I remove the Introsection (with the async asset loading) the <Suspense> directly resolves (before onMounted()) and the "target" ref returns the referenced <section> directly in the onMounted() hook (in the onResolve() hook both refs are undefined).

What am I doing wrong? Appreciate any useful hint/solution.

My code (shortend):

import IntroSection from '@/components/IntroSection.vue'
import { useTemplateRef, onBeforeUpdate, onUpdated, onMounted, ref } from 'vue'

const target = ref<Element>() // or useTemplateRef('target')
const loader = ref<Element>()
const animate = ref<boolean>(true)

onMounted(() => {
  console.log('onMounted')
  console.log(target.value)
  console.log(loader.value)
})

onBeforeUpdate(() => {
  console.log('onBeforeUpdate')
})

onUpdated(() => {
  console.log('onUpdated')
})

function onPending() {
  console.log('onPending...')
}
function onResolve() {
  console.log('onResolve...')
  console.log(target.value)
  console.log(loader.value)
}

function onReject() {
  console.log('onReject')
}
</script>

<template>
  <Suspense :onPending="onPending" :onResolve="onResolve" :onReject="onReject">
    <template #default>
      <div>
        <!-- Spacer Section -->
        <section class="header_padding">
          <div class="container">
            <div class="row title"></div>
          </div>
        </section>
        <!-- End Spacer Section -->

        <!-- Intro Section with async asset loading -->
        <IntroSection>
          <div class="row title">
            <h1 class="header_headline_style_2">
              HEADLINE
            </h1>
            <p>
              <br />
              Some Text here
            </p>
            <br />
          </div>
        </IntroSection>
        <!-- End Intro Section -->

        <!-- Service Section -->
        <section ref="target">
          <div v-if="animate" class="nf-item spacing">
            <div class="box_module_1">
              <div class="box_module_2">
                <h2 class="h2_box">TITLE</h2>
                <p>TEXT</p>
              </div>
              <div class="box_module_2">
                <RouterLink to="/text" class="btn">
                  BUTTONTEXT
                </RouterLink>
              </div>
            </div>
          </div>
        </section>
        <!-- End Service Section -->
      </div>
    </template>

    <template #fallback>
      <!-- Preloader -->
      <section id="preloader">
        <div class="loader" id="loader" ref="loader">
          <div>Loading...</div>
          <div class="loader-img"></div>
        </div>
      </section>
      <!-- End Preloader -->
    </template>
  </Suspense>
</template>

Solution

  • To access a ref within a <Suspense> component after async components have loaded, use the nextTick function within the onResolve hook to ensure the DOM has updated. Ensure your ref is correctly assigned to the intended element. Debugging with console.log in onResolve can help verify the timing and availability of your ref. This approach helps manage the asynchronous nature of components and their impact on Vue's reactivity system.

    import { nextTick } from 'vue';
    
    function onResolve() {
      nextTick(() => {
        console.log(target.value); // Now, target should be defined
      });
    }