SPA suspense works well, but SSR suspense is not ideal (with router keepAlive)
fengjac opened this issue · comments
What happened?
-
$
quasar dev
(SPA mode - no problem)
When the web page show, take turn to click "first" & "second" button quickly and immediately.
suspense #fallback template works well. It renders a loading state until async component to be resolved. Perfect. -
$
quasar dev -m ssr
(SSR mode - two problems)
Problem 1: It can not render a loading state when the web page show (first route is in pending);
Problem 2: When the web page show, take turn to click "first" & "second" button quickly and immediately. No loading state and Browser console Error happens like:
vue-router.mjs:3479 TypeError: Cannot read property 'shapeFlag' of null
and
TypeError: Cannot read property 'parentNode' of null
What did you expect to happen?
SSR suspense works well like SPA (with router keepAlive), no error, render loading state as expect
Reproduction URL
https://github.com/fengjac/spa-ssr-suspense-keepalive-demo
How to reproduce?
-
$ pnpm create quasar
√ What would you like to build? » App with Quasar CLI, let's go!
√ Project folder: ... spa-ssr-suspense-keepalive-demo
√ Pick Quasar version: » Quasar v2 (Vue 3 | latest and greatest)
√ Pick script type: » Typescript
√ Pick Quasar App CLI variant: » Quasar App CLI with Vite 5 (BETA | next major version - v2)
√ Package name: ... spa-ssr-suspense-keepalive-demo
√ Project product name: (must start with letter if building mobile apps) ... Quasar App
√ Project description: ... A Quasar Project
√ Pick a Vue component style: » Composition API with <script setup>
√ Pick your CSS preprocessor: » Sass with SCSS syntax
√ Check the features needed for your project: » Linting (vite-plugin-checker + ESLint + vue-tsc)
√ Pick an ESLint preset: » Prettier -
Add two routers and use keepAlive and suspense
<q-page class="column flex-center">
<q-tabs>
<q-route-tab name="first" label="First" to="/first" exact />
<q-route-tab name="second" label="Second" to="/second" exact />
</q-tabs>
<RouterView v-slot="{ Component, route }">
<template v-if="Component">
<KeepAlive :include="includeKeepAliveRoute">
<Suspense :timeout="0">
<template #default>
<component :is="Component" :key="route.name"></component>
</template>
<template #fallback>
<div>Show loading here ...</div>
</template>
</Suspense>
</KeepAlive>
</template>
</RouterView>
</q-page>
- After route component mounted, add route name to includeKeepAliveRoute by event bus
// first & second route component sleeps 3 seconds in setup
<script setup lang="ts">
await sleep(3000);
onMounted(() => {
bus.emit(KEEP_ALIVE_EVENT, COMPONENT_NAME);
});
// index page
bus.on(KEEP_ALIVE_EVENT, (name) => {
console.debug(`[${name}] takes keepalive bus.`);
if (!includeKeepAliveRoute.value.includes(name)) {
includeKeepAliveRoute.value.push(name);
}
});
- $ quasar dev -m ssr
- take turn to click "first" and "second" route tab when the web page is mounted immediately
Flavour
Quasar CLI with Vite (@quasar/cli | @quasar/app-vite)
Areas
SPA Mode, SSR Mode
Platforms/Browsers
Chrome
Quasar info output
Operating System - Windows_NT(10.0.19041) - win32/x64
NodeJs - 18.17.1
Global packages
NPM - 9.6.7
yarn - 1.22.19
@quasar/cli - 2.4.0
@quasar/icongenie - Not installed
cordova - Not installed
Important local packages
quasar - 2.15.1 -- Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
@quasar/app-vite - 2.0.0-beta.5 -- Quasar Framework App CLI with Vite
@quasar/extras - 1.16.9 -- Quasar Framework fonts, icons and animations
eslint-plugin-quasar - Not installed
vue - 3.4.21 -- The progressive JavaScript framework for building modern web UI.
vue-router - 4.3.0
pinia - Not installed
vuex - Not installed
vite - 5.2.6 -- Native-ESM powered web dev build tool
vite-plugin-checker - Not installed
eslint - 8.57.0 -- An AST-based pattern checker for JavaScript.
esbuild - 0.20.2 -- An extremely fast JavaScript and CSS bundler and minifier.
typescript - 5.3.3 -- TypeScript is a language for application scale JavaScript development
workbox-build - Not installed
register-service-worker - 1.7.2 -- Script for registering service worker, with hooks
electron - Not installed
electron-packager - Not installed
electron-builder - Not installed
@capacitor/core - Not installed
@capacitor/cli - Not installed
@capacitor/android - Not installed
@capacitor/ios - Not installed
Quasar App Extensions
*None installed*
Relevant log output
vue-router.mjs:3479 TypeError: Cannot read property 'shapeFlag' of null
at getNextHostNode (runtime-core.esm-bundler.js:6641)
at getNextHostNode (runtime-core.esm-bundler.js:6642)
at Object.next (runtime-core.esm-bundler.js:1608)
at getNextHostNode (runtime-core.esm-bundler.js:6645)
at getNextHostNode (runtime-core.esm-bundler.js:6642)
at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:6097)
at ReactiveEffect.run (reactivity.esm-bundler.js:177)
at instance.update (runtime-core.esm-bundler.js:6135)
at callWithErrorHandling (runtime-core.esm-bundler.js:195)
at flushJobs (runtime-core.esm-bundler.js:402)
TypeError: Cannot read property 'parentNode' of null
at parentNode (runtime-dom.esm-bundler.js:39)
at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:6095)
at ReactiveEffect.run (reactivity.esm-bundler.js:177)
at instance.update (runtime-core.esm-bundler.js:6135)
at callWithErrorHandling (runtime-core.esm-bundler.js:195)
at flushJobs (runtime-core.esm-bundler.js:402)
Additional context
No response
Hi,
This is a Vue bug, not a Quasar one.
Hi @rstoenescu
After I feedback this problem to Vue team, some one has comment like this:
"preFetch is handled by the Quasar framework internally, it is not a part of Vue. It seems that components with preFetch are not converted to async components by Quasar and Suspense works only with async components or async setup function" -- vuejs/core#10667
So could you have a look again? Thx !
At least in your example, u're not using preFetch, what is ok, since u're using suspense, but the comment about the preFetch hasn't effect here.
The down side of work with suspense instead of the preFetch, is the preFetch will skip if that was be executed in the server side.
With suspense, u'll need to write your own preFetch function, here a simple one:
src/composables/prefetch
import { type Ref, type InjectionKey, inject } from 'vue'
export const isFetchEnabledKey: InjectionKey<Ref<boolean>> = Symbol('is-fetch-enabled-key')
export function useIsFetchEnabled () {
const isFetchEnabled = inject(isFetchEnabledKey)
if (!isFetchEnabled) {
throw 'is fetch enabled was not be injected'
}
return isFetchEnabled
}
export async function usePreFetch <T> (fetchFn: () => Promise<T>): Promise<T | undefined> {
const isFetchEnabled = useIsFetchEnabled()
if (isFetchEnabled.value) {
return await fetchFn()
}
}
src/boot/prefetch
import { boot } from 'quasar/wrappers'
import { isFetchEnabledKey } from 'src/composables/prefetch'
import { ref } from 'vue'
export default boot(({ app, router }) => {
const isFetchEnabled = ref(process.env.MODE !== 'ssr' || !!process.env.SERVER)
app.provide(isFetchEnabledKey, isFetchEnabled)
if (process.env.MODE === 'ssr' && !!process.env.CLIENT) {
router.isReady().then(() => {
setTimeout(() => {
isFetchEnabled.value = true
}, 0)
})
}
})
so, in your async comp:
import { useSampleStore } from 'src/stores/sample'
import { usePreFetch } from 'src/composables/prefetch'
const sampleStore = useSampleStore()
await usePreFetch(() => sampleStore.fetchSamples())
I didn't tried that with Vite 5, but with the Vite 2, there was some inconsistencies with vue-router + ssr + suspense, specially when i tried to use async components in the router (a.k.a. Pages and Layouts)
NOT WORK: suspense directly in the router component
https://github.com/TobyMosque/ws-pets/blob/main/app/src/pages/BrokenSuspensePage.vue
WORK: suspense in the component imported by the router component
https://github.com/TobyMosque/ws-pets/blob/main/app/src/pages/SuspensePage.vue