Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
fengjac opened this issue Mar 28, 2024 · 3 comments
Closed
Labels
bug/1-hard-to-reproduce A reproduction is available, but it's hard to reproduce, so it has a lower priority. bug/1-repro-available A reproduction is available and needs to be confirmed. flavour/quasar-cli-vite kind/bug 🐞 mode/spa mode/ssr Qv2 🔝 Quasar v2 issues

Comments

@fengjac
Copy link

fengjac commented Mar 28, 2024

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

@fengjac fengjac added kind/bug 🐞 Qv2 🔝 Quasar v2 issues labels Mar 28, 2024
@github-actions github-actions bot added bug/1-hard-to-reproduce A reproduction is available, but it's hard to reproduce, so it has a lower priority. bug/1-repro-available A reproduction is available and needs to be confirmed. flavour/quasar-cli-vite mode/spa mode/ssr labels Mar 28, 2024
@rstoenescu
Copy link
Member

Hi,

This is a Vue bug, not a Quasar one.

@fengjac
Copy link
Author

fengjac commented Apr 9, 2024

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 !

@TobyMosque
Copy link
Contributor

TobyMosque commented Apr 9, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug/1-hard-to-reproduce A reproduction is available, but it's hard to reproduce, so it has a lower priority. bug/1-repro-available A reproduction is available and needs to be confirmed. flavour/quasar-cli-vite kind/bug 🐞 mode/spa mode/ssr Qv2 🔝 Quasar v2 issues
Projects
None yet
Development

No branches or pull requests

3 participants