nuxt: [Vue warn]: Hydration node mismatch:

Environment


  • Operating System: Linux
  • Node Version: v14.18.1
  • Nuxt Version: 3.0.0-27243104.5e903ae
  • Package Manager: Yarn
  • Bundler: Vite
  • User Config: meta, buildModules
  • Runtime Modules: -
  • Build Modules: nuxt-windicss@2.0.2

Describe the bug

A warning occurs in console when i add a @click attribute in html tag

[Vue warn]: Hydration node mismatch:
- Client vnode: i 
- Server rendered DOM: <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu">
  <line x1="3" y1="12" x2="21" y2="12"></line>
  <line x1="3" y1="6" x2="21" y2="6"></line>
  <line x1="3" y1="18" x2="21" y2="18"></line>
</svg>  
  at <Navbar> 
  at <AsyncComponentWrapper> 
  at <Default name="default" > 
  at <AsyncComponentWrapper name="default" > 
  at <NuxtLayout key=0 name=undefined > 
  at <RouterView> 
  at <NuxtPage> 
  at <App> 
  at <Root>

Reproduction

.

Additional context

<template>
  ... 
  <a @click="menu"><i data-feather="menu"></i></a>
</template>
<script setup>
  const menu = () => {
    console.log('menu')
  }
</script>

Logs

No response

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 23 (4 by maintainers)

Most upvoted comments

@henningko

This is a fairly common hydration mistake. Making these mistakes comes from a misunderstanding of how SSR and hydration actually work.

So, I’ll try to explain briefly.

Your code:

<template>
  <div class=".1">
    <div v-if="isAuthenticated()">
      <logout />
      <h1 class=".2">.3</h1>
      <Content />
    </div>
    <div class="auth" v-else>.4</div>
  </div>
</template>

<script setup lang="ts">
  const { $supabase } = useNuxtApp();
  const loading = ref(false);
  const { isAuthenticated } = await useAuth();
</script>

When Nuxt does an SSR for this page, it generates an HTML structure:

<div class=".1">
  <div class="auth">.4</div>
</div>

Because isAuthenticated will always be false because the work happens on the server, and there are no cookies on the server, no tokens, nothing to do with authentication.

Next, Nuxt will pass this code to the client as HTML and JS.

But the trick is that the HTML structure is now “dead”. That is, it is not connected in any way with the state, there are no listeners on it. It’s just non-interactive HTML. The process of “revitalizing” the HTML structure is called “hydration”.

When hydrated, Vue doesn’t re-render the HTML, but rather “picks up”.

The pickup goes like this: Vue has a virtual component tree (virtual DOM) and just recursively goes through it, connecting the real DOM node to the virtual DOM node along the way.

The v-if directive doesn’t just add style="display: none" (as the v-show directive does), but completely cuts the node physically from the DOM.

Also, the setup code is executed BEFORE hydration or rendering. That is, the authorized client receives true in v-if, unlike the server.

So if the client is authorized, when Vue tries to hydrate your component, it will try to synchronize the next virtual DOM:

<div class=".1">
   <div>
     <logout />
     <h1 class=".2">.3</h1>
     <Content />
   </div>
</div>

With the actual (which came from the server):

<div class=".1">
   <div class="auth">.4</div>
</div>

Which will cause a sync error.


As a quick fix, consider using v-show instead of v-if. But this solution is bad for several reasons. For example, there is no v-show-else. Secondly, style="display: none" will solve the hydration problem, but if you have a lot of elements that depend on authorization, then using v-show will significantly increase both the number of DOM nodes, which will slow down the page, and the virtual size DOM, each node of which is served by reactivity.

As the right solution, it is worth considering the migration of authorization to the client in full:

<template>
   <div class=".1">
     <div v-if="isAuthenticated">
       <logout />
       <h1 class=".2">.3</h1>
       <Content />
     </div>
     <div class="auth" v-else>.4</div>
   </div>
</template>

<script setup lang="ts">
   import { ref } from 'vue';

   const { $supabase } = useNuxtApp();
   const loading = ref(false);
   const { isAuthenticated: checkIsAuthenticated } = await useAuth();
   const isAuthenticated = ref(false);

   onMounted(() => {
     isAuthenticated.value = checkIsAuthenticated();
   });
</script>

The onMounted hook only runs on the client.

Even better, wrap your component in a custom <ClientOnly></ClientOnly> tag.

Hope it’s a little clearer.)

tracking need for <client-only> at https://github.com/nuxt/nuxt.js/issues/11884

Trying to wrap my head around this…:

[Vue warn]: Hydration node mismatch:
- Client vnode: div 
- Server rendered DOM: 
<form class="row flex flex-center"> <empty string> 
  at <Index onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< undefined > > 
  at <BaseTransition mode="out-in" appear=false persisted=false  ... > 
  at <Transition name="page" mode="out-in" > 
  at <RouterView > 
  at <NuxtPage> 
  at <App> 
  at <NuxtRoot>

Which probably stems from a problem with my if/else, as the form referenced up top is the form shown if isAuthenticated() fails:

<template>
  <div class="w-full min-h-screen bg-white dark:bg-black dark:text-white p-8">
    <div v-if="isAuthenticated()">
      <Logout />
      <h1 class="text-7xl font-bold mb-8">Brief.</h1>
      <Content />
    </div>
    <Auth v-else />
  </div>
</template>
<script setup lang="ts">
const { $supabase } = useNuxtApp();
const loading = ref(false);
const { isAuthenticated } = await useAuth();
</script>

useAuth.ts

import type { Session } from "@supabase/supabase-js";

// const authenticated = ref<boolean>(false);

// session is null when not logged in
const userSession = ref<Session | null>(null);
const userCookieSet = ref<boolean | null>(null);

export default async () => {
  const isAuthenticated = () => {
    return userSession.value?.user && userCookieSet.value;
  };

  return {
    userSession,
    userCookieSet,
    isAuthenticated,
  };
};

whereby userSession is set by supabase.auth, and a subsequent request to /api/auth ensures that a cookie is being set so that Nuxt’s API can make requests to supabase on behalf of the user. Full repo: https://github.com/henningko/briefxyz/tree/api New to Nuxt3/SSR/Supabase and biting off more than I can chew, probably, so please don’t hold back with criticism 😃

Environment

  • Operating System: macOS
  • Node Version: v17.4.0
  • Nuxt Version: 3.0.0-27398533.8edd481
  • Package Manager: Yarn
  • Bundler: Vite
  • User Config: buildModules, RuntimeConfig
  • Runtime Modules: -
  • Build Modules: nuxt-windicss@2.2.2

I had the same issue. I tried wrapping the div with ClientOnly and it works. I don’t see the warning in the console log anymore.

<ClientOnly>
	<div v-if="auth.user">
		<div class="font-bold text-sm">{{ auth.user.name }}</div>
		<div class="text-xs text-slate-400 font-light">{{ auth.user.email }}</div>
	</div>
 </ClientOnly>

I had this problem with font-awesome icons. My workaround was this:

<font-awesome-icon
    v-if="!loading"
    icon="fa-solid fa-phone"
/>
<script lang="ts" setup>
const loading = ref(true);

const nuxtApp = useNuxtApp();
nuxtApp.hook("page:finish", () => {
  loading.value = false;
});
</script>

This waits for the page to finish, i.e. the content to be present client-side.

If you want to use the whole feather-icons package, I think it’s easiest to implement a component which renders the icon to SVG, like so:

<script>
import feather from "feather-icons";

export default {
  props: {
    icon: String,
    validator: function (value) {
      return typeof feather.icons[value] !== "undefined";
    },
  },
  render(h) {
    let svg = "";
    if (typeof feather.icons[this.icon] !== "undefined") {
      svg = feather.icons[this.icon].toSvg();
    }
    return h("i", {
      class: {
        icon: true,
      },
      domProps: {
        innerHTML: svg,
      },
    });
  },
};
</script>

<style scoped>
i {
  display: inline-block;
}
</style>

which could be used as follows

<template>
  <div id="app">
    <Feather-Icon icon="x" />
    <Feather-Icon icon="y" />
    <Feather-Icon icon="award" />
    <Feather-Icon icon="briefcase" />
  </div>
</template>

<script>
import FeatherIcon from "./components/FeatherIcon";

export default {
  name: "App",
  components: {
    FeatherIcon,
  },
};
</script>

<style>
.icon {
  color: #364fc7;
}
</style>

see https://codesandbox.io/s/happy-kare-hoy65?file=/src/components/FeatherIcon.vue:0-536

There are also other packages like vue-feather, but IMHO those packages often provide features which are not always required in every project…

@sanjarbarakayev,

Let’s simplify the context to focus on the problem. Let’s look at the following problematic component pages/page.vue:

/app.vue

<template>
  <NuxtPage />
</template>

pages/page.vue

<template>
  <div>
    <p>Data...</p>
    <div v-if="loading">loading</div>
    <div v-else>not loading</div>
  </div>
</template>

<script lang="ts" setup>
  // Some random value, like loading, etc
  const someComputedValue = ref(Math.random() > 0.5);

  const loading = computed(() => someComputedValue.value);
</script>

When reloading the page, sometimes (with a ~50% chance) a hydration error will occur. About the same as yours.

You need to synchronize the loading value on the server and client.

So, there are three ways to implement this synchronization, depending on your context:

  • ClientOnly
  • onMounted
  • load only on client

ClientOnly

Disable SSR altogether for the problem area.

pages/page.vue

<template>
  <div>
    <p>Data...</p>

    <ClientOnly>
      <div v-if="loading">loading</div>
      <div v-else>not loading</div>

      <template #fallback>
        <Loading />
      </template>
    </ClientOnly>
  </div>
</template>

<script lang="ts" setup>
  // Some random value, like loading, etc
  const someComputedValue = ref(Math.random() > 0.5);

  const loading = computed(() => someComputedValue.value);
</script>

onMounted

pages/page.vue

<template>
  <div>
    <p>Data...</p>

    <div v-if="loading">loading</div>
    <div v-else>not loading</div>
  </div>
</template>

<script lang="ts" setup>
  // Some random value, like loading, etc
  const someComputedValue = ref(Math.random() > 0.5);

  const clientEnv = ref(false);
  onMounted(() => (clientEnv.value = true));

  const loading = computed(() => {
    if (!clientEnv.value) {
      return true;
    }

    return someComputedValue.value;
  });
</script>

You might figure out that the code is executed differently on the client, but in general the method looks like this. Essentially the same as ClientOnly, but your <div>loading</div> will be rendered on the server. Do you need your <div>loading</div> in SEO? This is a big question.

load only on client

In this case, you ensure that notificationStore.loading on the server is always the same as in setup on the client. This is achieved, for example, this way:

/app.vue

<template>
  <NuxtPage />
</template>

<script setup>
  import useNotificationStore from '~/stores/notification';

  const notificationStore = useNotificationStore();
  onMounted(() => notificationStore.loadNotifications());
</script>

Now throughout the entire project you won’t have hydration problems due to notificationStore.loading.


References:


Which to choose?

Without knowing your context, I suspect that notificationStore is needed to display client notifications. In this case, rendering loaders will be useless for SEO, and the SEO bot will always be without an account, i.e. it will see something like: ‘You have no notifications.’

In this case, the option with ClientOnly is suitable for you.

If you need SEO to display notifications, then I recommend the load only on client option.

I had the same issue. I tried wrapping the div with ClientOnly and it works. I don’t see the warning in the console log anymore.

<ClientOnly>
	<div v-if="auth.user">
		<div class="font-bold text-sm">{{ auth.user.name }}</div>
		<div class="text-xs text-slate-400 font-light">{{ auth.user.email }}</div>
	</div>
 </ClientOnly>

This works for FontAwesome Items in Nuxt 3 , Thanks for sharing

I got this error by using conditional logic inside a named slot in a base container template, like this:

<BaseContainer.vue>
  <slot name="loading">
      <template v-if="loading">
        loading...
        <!--<BaseLoadingFeather />-->
      </template>
    </slot>
    ...other slots
 </BaseContainer>

Note how I thought like others that it might have been the feather library (loading in a separate component) that was the issue. Negative, removing that library had no effect. The issue was having the v-if=“loading” inside the named slot. Upon removing the conditional logic from the Base template and moving that up to the parent template, no hydration error. Note in the above, I’m receiving loading as a prop. I may have been wrong here in the first place regarding conditional rendering inside a slot but still thought this was worth sharing in case someone else encounters the same. No v-if / conditional rendering inside named slots (or at least not in this example).

Edit: More testing, identified that I can add conditional logic on BaseLoadingFeather component that loads the feather library without use of onNuxtReady if the loading slot is wrapped in a ClientOnly component. Thought that I had tested that specific (seems no), passing without hydration issues like this inside a base component template. Sharing in case this helps someone.

// ShowRecordContainer
<ClientOnly>
  <slot name="loading">
    <BaseLoadingFeather v-if="loading" />
  </slot>
</ClientOnly>

Use case is like this:

//student/[slug].vue
 <ShowRecordContainer
    :loading="loading"
    @close="emit('close')"
  >
  <template #header>....
  <template #default>....
  // nice, the loader works in all show record pages of various data types without issue.

You should pay attention to the warnings and add some handling on the client side to avoid updating the variable until your app has mounted. You can use onNuxtReady composable in the next Nuxt release to do so: https://github.com/nuxt/framework/pull/9478.