Handling simple hydration mismatch after fetch in Nuxt 3 – Javascript

by
Ali Hasan
client-side javascript nuxt.js nuxtjs3 server-side-rendering

Quick Fix: Specify the page as a CSR-page-component in nuxt.config.ts to render the page only on the client side, causing $fetch to execute in users’ browsers. Alternatively, use useLazyFetch along with wrapping the element in <client-only> to achieve the same result.

The Problem:

In a Nuxt 3 application, a confirmation page for email verification is implemented. The page sends a POST request to an API to validate the confirmation key and displays a success message upon successful validation. However, the confirmation message disappears after a few milliseconds with a ‘hydration mismatch’ warning in the console. This is because the page is initially rendered on the server with the confirmation message, but when the page is rendered again in the browser, a second API call is made and the validation fails due to the key being removed from the database. The challenge is to resolve the hydration mismatch issue and maintain the confirmation message after the initial render.

The Solutions:

Solution 1: Specify the page as a CSR-page-component

In nuxt.config.ts, specify the pages/confirm/[key].vue as a CSR-page-component by adding the following code:

export default defineNuxtConfig({
    routeRules: {
        'confirm-key': {
            ssr: false
        }
    }
});

By doing this, the page will only be rendered on the client side, eliminating the hydration mismatch issue. The $fetch call will only execute in users’ browsers, ensuring that the confirmation message is displayed correctly.

Solution 2: Use `useLazyFetch` and ``

In the pages/confirm/[key].vue file, use the useLazyFetch composable instead of $fetch. Additionally, wrap the <div> containing the confirmation message in the <client-only> component. The code should look like this:

// pages/confirm/[key].vue

<script setup>
    const route = useRoute();
    const key = route.params.key;

    const confirmed = ref(false);

    try {
        const result = await useLazyFetch('/api/confirm', {
            method: 'post',
            body: { key: key },
        });

        confirmed.value = !!result;
    } catch {}
</script>

<template>
    <client-only>
        <div v-if="confirmed">Email confirmed!</div>
    </client-only>
</template>

The useLazyFetch composable ensures that the API call is only made on the client side, preventing the hydration mismatch issue. The <client-only> component ensures that the confirmation message is only rendered on the client side.

Solution 3: Verify the key on the server side

You can also verify the confirmation key on the server side using the useNuxtApp and callWithNuxt composables. This approach is more secure as it prevents potential safety issues associated with verifying the key on the client side. The code should look like this:

// pages/confirm/[key].ts

import { useNuxtApp, callWithNuxt } from '#app';

definePageMeta({
    middleware: [
        async (to: RouteLocationNormalized) => {

            const nuxtApp = useNuxtApp();

            const { data } = await useFetch(
                '/api/confirm',
                {
                    method: 'POST',
                    body: { key: to.params.key },
                }
            );

            await callWithNuxt(
                nuxtApp,
                () => {
                    nuxtApp.$verifyResult = data.value;
                }
            );
        }
    ]
});

const nuxtApp = useNuxtApp();
const route = useRoute();

const verifyResult: Ref<boolean> = ref(nuxtApp.$verifyResult ?? false);

if (verifyResult.value) {
    navigateTo('/confirm/success.vue');
} else {
    navigateTo('/confirm/failed.vue');
}

In this approach, the API call to verify the key is made on the server side, ensuring that the confirmation message is displayed correctly without any hydration mismatch issues.

Solution 2: Use `useFetch` Composables

Instead of using the $fetch function, use the useFetch composable. It automatically dedupes server/client API calls, eliminating hydration mismatch and improving app performance. In your example, this change would resolve the issue of the “Email confirmed” message disappearing after a few milliseconds.

Example:

pages/confirm/[key].vue

<script setup>
  const route = useRoute();
  const key = route.params.key;

  const confirmed = ref(false);

  const { data: result } = useFetch('/api/confirm', {
    method: 'post',
    body: { key: key },
  });

  confirmed.value = !!result?.value;
</script>

<template>
  <div v-if="confirmed">Email confirmed!</div>
</template>

Explanation:

  • The useFetch composable is imported.
  • Within useFetch, the API call to '/api/confirm' is made with the appropriate options, including the HTTP method and request body.
  • The composable returns a response object, which is destructured to obtain the data property.
  • The data property is used to set the confirmed ref value, taking into account the existence and truthiness of the result.

This revised solution eliminates the hydration mismatch issue by automatically deduping the API call on the client-side, ensuring that the "Email confirmed" message persists after the initial render.

Q&A

How to fix hydration mismatch when confirming an email in Nuxt 3?

Specify the page as a CSR-page-component in nuxt.config.ts or use useLazyFetch with &lt;client-only&gt;.

Why using useFetch instead of $fetch is recommended?

useFetch automatically dedupes server and client API calls, improving performance and eliminating hydration mismatches.

Video Explanation:

The following video, titled "Error Handling in Nuxt 3 - YouTube", provides additional insights and in-depth exploration related to the topics discussed in this post.

Play video

Error Handling in Nuxt 3 00:00 Introduction to Error Handling 00:46 createError 02:28 NuxtErrorBoundary 03:46 Client Side Error Handling ...