quasarframework / quasar

Quasar Framework - Build high-performance VueJS user interfaces in record time

Home Page:https://quasar.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

SPA suspense works well, but SSR suspense is not ideal (with router keepAlive)

fengjac opened this issue · comments

What happened?

  1. $ 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.

  2. $ 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?

  1. $ 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

  2. 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>
  1. 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);
  }
});
  1. $ quasar dev -m ssr
  2. 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