nuxt / ui

A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.
https://ui.nuxt.com
MIT License
4.13k stars 544 forks source link

SSR Hydration Mismatch on Dynamic Named Templates using Async Data #376

Closed MadDog4533 closed 1 year ago

MadDog4533 commented 1 year ago

Background

I know my use case is probably pretty niche but when dynamically rendering a template for instance on a UCard, the <component :is="as" ...> wrapper introduces a

[Vue warn]: Hydration node mismatch:
- Client vnode: div 
- Server rendered DOM: <!--]-->

and delays rendering of the template content. I am fetching dynamic content from a headless CMS (storyblok) using an useAsyncData or equivalent await useAsyncStoryblok and passing the data down the component chain. Copying the source of UCard, creating a new component, and replacing the <component ...> with a standard div wrapper fixes the hydration error. Or, ssr: false fixes the problem.

Version

@nuxthq/ui: 2.3.0 nuxt: nuxt@3.5.3

Reproduction Link

Proxy Card Component that handles incoming async /storyblok/Card.vue

<template>
    <UCard>
        <template v-for="template in templates" v-slot:[`${template.name}`]>
            <div v-if="template">
                <StoryblokComponent v-for="slotBlok in tempate.slot" :key="slotBlok._uid" :blok="slotBlok" />
            </div>
        </template>
        <StoryblokComponent v-for="cardBlok in blok.CardContents" :key="cardBlok._uid" :blok="cardBlok" />
    </UCard>
</template>
<script setup>
    const props = defineProps({ blok: Object });

    const templates = computed(() => {
        return props.blok.CardContents.filter((el, index, arr) => {
            return el.component == "template";
        });
    });

    const ui = {
        "base": "",
        "background": ""
    }
</script>

Slug Page that retrieves all found storyblok content /pages/[...slug].vue

<template>
  <StoryblokComponent v-if="story" :blok="story.content" class="prose"/>
</template>
<script setup lang="ts">
    const route = useRoute();
    const story = await useAsyncStoryblok(route.path, { version: "draft" });

    if (!story.value && !route.matched)
        throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })

</script>

Note: useAsyncStoryblok as outlined here

A 'fixed' copy of UCard

<template>
  <div
    :class="[ui.base, ui.rounded, ui.divide, ui.ring, ui.shadow, ui.background]"
    v-bind="$attrs"
  >
    <div v-if="$slots.header" :class="[ui.header.base, ui.header.padding, ui.header.background]">
      <slot name="header" />
    </div>
    <div :class="[ui.body.base, ui.body.padding, ui.body.background]">
      <slot />
    </div>
    <div v-if="$slots.footer" :class="[ui.footer.base, ui.footer.padding, ui.footer.background]">
      <slot name="footer" />
    </div>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
export default defineComponent({
  inheritAttrs: false,
  props: {
    as: {
      type: String,
      default: 'div'
    },
    ui: {
      type: Object as PropType<Partial<typeof appConfig.ui.card>>,
      default: () => appConfig.ui.card
    }
  },
  setup (props) {
    // TODO: Remove
    const appConfig = useAppConfig()
    const ui = computed<Partial<typeof appConfig.ui.card>>(() => defu({}, props.ui, appConfig.ui.card))
    return {
      // eslint-disable-next-line vue/no-dupe-keys
      ui
    }
  }
})
</script>

Steps to reproduce

1) Fresh nuxt 3 with nuxt UI 2) Add storyblok module 3) Dynamically hydrate named templates from async data. 4) Browser throws an SSR Hydration Error and delays rendering.

What is Expected?

Instant rendering and matching hydration from async data.

What is actually happening?

Delayed rendering and hydration mismatch error on named templates.

Potential Fix?

Maybe include a v-if switch to signal the use of <component :is="as" ...> if the as prop is defined on associated components?

benjamincanac commented 1 year ago

Would you mind sharing a reproduction?