最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Suspense in Vue 3 on nested routes - why content disappears? - Stack Overflow

programmeradmin2浏览0评论

I'm using Suspense, and fetching async data on pages. On init fallback loading indicator is visible, but while switching pages I'm keeping current page content until new page is ready + nprogress bar, so site behaves more like SSR. There is also Transition between, but it's not important for that issue.

And it works great. Problem is when my page has child pages (nested routes), also with Suspense. When I'm on nested page like /demo/demo1 and navigate to home, content of /demo template stays like it should, but content of subpage demo1 disappears. I need to keep nested Suspense until parent Suspense is ready. Any ideas?

I simplified code as much as possible. I'm using same <RouterView> code on both root App and page.

<RouterView v-slot="{ Component }">
  <template v-if="Component">
    <Suspense>
      <ponent :is="Component" />
      <template #fallback>
        Loading...
      </template>
    </Suspense>
  </template>
</RouterView>

Here is live reproduction: /pages/demo/demo1.vue (there is no nprogress, just wait 1s between pages).

Steps to reproduction:

  1. Click on "About" - "home page" stays until "about" is ready, and then "about" appears - works OK.
  2. Click on "Demo" - "demo" root page appears, fallback for nested "demo/demo1" route appears - works OK.
  3. Click on "Home" - content of "demo/demo1" immediately disappears.

I'm using Suspense, and fetching async data on pages. On init fallback loading indicator is visible, but while switching pages I'm keeping current page content until new page is ready + nprogress bar, so site behaves more like SSR. There is also Transition between, but it's not important for that issue.

And it works great. Problem is when my page has child pages (nested routes), also with Suspense. When I'm on nested page like /demo/demo1 and navigate to home, content of /demo template stays like it should, but content of subpage demo1 disappears. I need to keep nested Suspense until parent Suspense is ready. Any ideas?

I simplified code as much as possible. I'm using same <RouterView> code on both root App and page.

<RouterView v-slot="{ Component }">
  <template v-if="Component">
    <Suspense>
      <ponent :is="Component" />
      <template #fallback>
        Loading...
      </template>
    </Suspense>
  </template>
</RouterView>

Here is live reproduction: https://stackblitz./edit/vitejs-vite-gg6kqo?file=src/pages/demo/demo1.vue (there is no nprogress, just wait 1s between pages).

Steps to reproduction:

  1. Click on "About" - "home page" stays until "about" is ready, and then "about" appears - works OK.
  2. Click on "Demo" - "demo" root page appears, fallback for nested "demo/demo1" route appears - works OK.
  3. Click on "Home" - content of "demo/demo1" immediately disappears.
Share Improve this question edited May 5, 2023 at 19:18 chojnicki asked May 5, 2023 at 19:05 chojnickichojnicki 3,6421 gold badge25 silver badges34 bronze badges 6
  • 1 It might be not working yet. github./vuejs/core/issues/5637 – Kalimah Commented May 10, 2023 at 7:46
  • 1 Vue v3.3.2, this now seems to be working as expected for me. – Marc Commented May 17, 2023 at 14:03
  • @Marc weird. I did tested alpha version before asking here, because they implemented "suspensible" option in 3.3.0. But it changed nothing in my issue. Will test it again with 3.3.2 then, with and without that option. thanks. – chojnicki Commented May 18, 2023 at 15:04
  • 1 I noticed a changelog in Nuxt3 explicitly mentioning bumping its Vue dependency version to 3.3.2 because a nested pages bug fix was released, but I saw no documented changes in the Vue 3 changelog its self for this issue. – Marc Commented May 18, 2023 at 18:45
  • 1 I updated my answer further down the thread with what I am doing, perhaps there is something there that might answer this, it defiantly working for me I put in production last week soon as I spotted the discrete fix. I assume they are quite on it because its not officially production ready. – Marc Commented May 26, 2023 at 10:14
 |  Show 1 more ment

2 Answers 2

Reset to default 3 +200

I wouldn't say this to be the best answer, but it seems to me that in order for <Suspense> to work, its child must be an asynchronous ponent. In this case, <RouterView> is rendered as a non-asynchronous ponent (even though its children are asynchronous ponents), thus the root <Suspense> wouldn't wait for the <Suspense> inside the <RouterView>.

As a workaround, I think we can cache the ponent, so that it wouldn't look as if it's gone when the router is changed (the third case).

My method of caching is by adding this <script setup> in the demo.vue file:

<script setup>
import { ref } from 'vue';

const CachedComponent = ref(null);

function cacheComponent(Component) {
  CachedComponent.value = Component || CachedComponent.value;
}
</script>

And then modify the <RouterView> inside the same file to this:

    ...
    <RouterView v-slot="{ Component }">
      {{ cacheComponent(Component) }}
      <template v-if="Component || CachedComponent">
        <Suspense>
          <ponent :is="Component || CachedComponent" />
          <template #fallback>
            <div style="font-size: 1.5rem">Loading Demo Nested...</div>
          </template>
        </Suspense>
      </template>
    </RouterView>
    ...

The {{ cacheComponent(Component) }} line will cache the ponent when it's rendered (it's used that way because <RouterView> doesn't have callback when its slots are mounted). The CachedComponent.value = Component || CachedComponent.value; line will update the CachedComponent value if there is a new Component that is not falsy. And if it's falsy (meaning that it's not mounted yet or not mounted anymore), it will render the previous latest cached version of the ponent. I know this is hacky, but in my opinion, this is a very simple workaround.

This is the forked stackblitz if you're interested.

Update Vue 3.3.2 - 17/05/2023

I just tested out this version and the nested router pages with async setup are working.

From what I can see, nested router pages pages are no longer displaying blank pages with a console warning about needing to be wrapped in Suspense.

Instead the top level Suspense is now handling the child pages without wrongly suggesting that there needed to be a Suspense around the nested RouterView.

// App.vue
<RouterView v-slot="{ Component, route }">
    <template v-if="Component">
        <KeepAlive>
            <Suspense>
                <ponent :is="Component"/>
                <template #fallback>
                    LOADING...
                </template>
            </Suspense>
        </KeepAlive>
    </template>
</RouterView>

// pages/nested-path/index.vue

<div>
  <RouterView v-slot="{ Component, route }">
    <ponent :is="Component" :key="route.path"/>
  </RouterView>
</div>

Previous Response

It simply does not work at this time for plex projects when you have nested routes, it works fine with routing on a single level as you have noticed.

I battled with this for a while, tried many binations and found nested suspense is broken, it is to do with a breakdown in the functionality around the key that is passively used for the router ponent, I tried setting this manually which yielded various results and other unfixable/annoying problems including any use with keep-alive.

Not being able to use async setup was really annoying and unfortunate especially for suspending while negotiating other promises before attempting an API call, I especially like how clean the code would have been.

I had to settle for async watch where I set a loading = ref(true) state passed to a custom loading state ponent wrapper that would I/O block content and show a loading progress in the content body.

// myPage.vue

<script setup lang="ts">

const props = defineProps<{
    id: string
}>()

const loading = ref(true)

watch(props, async () => {

  loading.value = true

  await myApi.endpoint.get(props)

  loading.value = false

}, {immediate: true})

</script>

<template>
  <content-loader :loading="loading">
    <div>
      My page content
    </div>
  </content-loader>
</template>
// ContentLoader.vue

<script setup lang="ts">

withDefaults(defineProps<{
    loading: boolean
}>(), {
    loading: true
})

</script>

<template>

    <div class="d-flex fill-height justify-center align-center">

        <template v-if="$props.loading">

            <span>Content loading...

        </template>

        <template v-else>
            <slot/>
        </template>

    </div>

</template>

You could slap transitions in there too if you wanted.

I was also thinking about re-deducing repetition of the loading wrapper and loading reference by moving it to the parent ponent hosting the child router-view and using emit in the child page to toggle loading. I haven't tried this, need to ensure the initial state is true when transitioning to other pages.

发布评论

评论列表(0)

  1. 暂无评论