N
Nuxt4w ago
lrobb

Hydration Mismatch Error on dev/demo envs, not locally

@kapa.ai and whomever else - I have a newer SSR Nuxt 3 app that uses Nuxt Apollo, and in particular I'm using useAsyncQuery. When I go to a couple of my routes directly or reload those pages, I receive the following hydration mismatch error: Dr-3uGvq.js:14 Hydration completed but contains mismatches. That's the only error feedback I'm given. Some of my routes have very similar code but the error doesn't happen, while it does on a couple other routes. NOTE: I'm not getting this error locally, only in my dev and demo environments (I haven't checked prod yet). Using Tailscale and GCP for hosting. 1. I am using new Date() in one of the components, but this error still occurs even when I remove that and hardcode the date. 2. When I comment out all of the HTML being used in the pages and related components, I still get the mismatch error. 3. When I move the actual query from the composable to the component, I still get the mismatch error. 4. When I put the actual query into an onMounted() in the component, the error goes away. 5. When I move actual query back to the composable and call the get from an onMounted() in the component, the error goes away but it wants me to use $fetch instead: [nuxt] [useAsyncData] Component is already mounted, please use $fetch instead. Any solutions I've found are becoming hacky, and I'm wondering if there is something else going on or if I really just need to do Item #4 above. (I'll add my code in a separate comment)
5 Replies
kapa.ai
kapa.ai4w ago
Hi @lrobb. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- Based on the information in the provided knowledge sources, hydration mismatch errors in Nuxt 3 are often caused by differences between the server-rendered HTML and what the client expects during hydration. Here are some key points and relevant findings from the sources that relate to your situation: 1. useAsyncData / useAsyncQuery Usage
Composables like useAsyncData (and by extension, useAsyncQuery) must be called at the top level of your setup function, not inside event handlers or lifecycle hooks like onMounted. If you call them after the component is mounted, Nuxt will warn you to use $fetch instead. This is because composables are designed to work with SSR and hydration, and calling them at the wrong time can break the SSR/client sync, leading to hydration mismatches. This matches your observation in item #5 and is confirmed in the sources:
useAsyncData (and by extension useAsyncQuery) is a composable, which means it can only be called at the top level of a setup function. You cannot call it within an event handler. When making requests within an event handler in Nuxt, $fetch is the correct approach.
Nuxt - Component is already mounted, please use $fetch instead 2. Hydration Mismatch Causes
Hydration mismatches can be caused by any difference in the rendered output between server and client. This includes: - Using non-deterministic values (like new Date(), Math.random(), or Date.now()) directly in the template or as initial values in refs. These should be set in onMounted to avoid SSR/client differences. - Mutating props or shared data in a way that changes the data between server and client renders (e.g., using shift() on a prop array)
Nuxt 3 Hydration text mismatch in ... error when using shift() Hydration completed but contains mismatches using VeeValidate and Pinia in Nuxt 3. Environment Differences
If the error only occurs in dev/demo environments and not locally, it could be due to differences in environment variables, build configuration, or even differences in how SSR is handled (e.g., different Node versions, caching, or deployment settings). However, the sources do not provide a definitive answer for environment-specific hydration mismatches. 4. Workarounds and Best Practices
- Avoid using useAsyncData/useAsyncQuery inside onMounted or after the component is mounted. If you need to fetch data after mount, use $fetch. - Ensure any non-deterministic values are only set on the client (e.g., in onMounted). - Do not mutate props or shared data directly in child components. - If you must fetch data only on the client, you can use <client-only> or set SSR to false for specific routes, but this is generally not recommended unless necessary. Summary:
Your workaround in item #4 (moving the query into onMounted) avoids the hydration mismatch because it only fetches data on the client, but this is not the recommended pattern for SSR in Nuxt. The correct approach is to use useAsyncQuery at the top level of setup for SSR-safe data fetching. If you still get hydration mismatches, double-check for any non-deterministic code or data mutations between server and client. If the issue persists only in certain environments, investigate differences in build or runtime configuration. If you need to fetch data after mount, use $fetch as recommended by Nuxt. If you provide more code, it may be possible to give more targeted advice. If you want to read more about data fetching and hydration in Nuxt, see the official docs: Nuxt Data Fetching and the related Stack Overflow discussions above. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai4w ago
<@1367905110705045598> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
lrobb
lrobbOP4w ago
Composable:
import { GetGroups as GET_GROUPS } from '@api/graphql/v1/groups.queries.gql'

export async function useGroups(organizationId) {
const groups = useState('groups', () => shallowRef(null))

if (!groups.value && !organizationId)
throw new Error(`Missing organizationId required to initialize composable 'useGroup'.`)

const get = async (organizationId) => {
try {
return await useAsyncQuery(GET_GROUPS, { organizationId }, { fetchPolicy: 'network-only' })
} catch (error) {
return null
}
}

if (organizationId) {
const { data } = await get(organizationId)
groups.value = data.value.groups
}

return {
groups,
refetch: get
}
}
import { GetGroups as GET_GROUPS } from '@api/graphql/v1/groups.queries.gql'

export async function useGroups(organizationId) {
const groups = useState('groups', () => shallowRef(null))

if (!groups.value && !organizationId)
throw new Error(`Missing organizationId required to initialize composable 'useGroup'.`)

const get = async (organizationId) => {
try {
return await useAsyncQuery(GET_GROUPS, { organizationId }, { fetchPolicy: 'network-only' })
} catch (error) {
return null
}
}

if (organizationId) {
const { data } = await get(organizationId)
groups.value = data.value.groups
}

return {
groups,
refetch: get
}
}
Page
<template>
<NuxtLayout
name="page"
title="Groups"
subtitle="Manage your organization's groups here."
>
<GroupsList :groups />
</NuxtLayout>
</template>

<script setup>
const { user } = await useUser()
const { groups } = await useGroups(user.value.organizationId)
</script>
<template>
<NuxtLayout
name="page"
title="Groups"
subtitle="Manage your organization's groups here."
>
<GroupsList :groups />
</NuxtLayout>
</template>

<script setup>
const { user } = await useUser()
const { groups } = await useGroups(user.value.organizationId)
</script>
@kapa.ai Above is my code for additional context
kapa.ai
kapa.ai4w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Thank you for providing your code. Based on the knowledge sources and your context, here are the key issues and recommendations: 1. useAsyncQuery in an Async Composable Your composable useGroups is async and calls useAsyncQuery inside an inner async function (get). However, composables like useAsyncData and useAsyncQuery are designed to be called synchronously at the top level of the setup function or composable, not inside an async function or after an await. This is crucial for SSR and hydration to work correctly in Nuxt 3. Calling them asynchronously or conditionally can break the SSR/client sync and cause hydration mismatches. This is confirmed in the knowledge sources:
useAsyncData (and by extension useAsyncQuery) is a composable, which means it can only be called at the top level of a setup function. You cannot call it within an event handler.
Nuxt - Component is already mounted, please use $fetch instead 2. Hydration Mismatch Hydration mismatches often occur when: - Data is fetched or mutated differently on the server and client. - Composables are called in a non-SSR-friendly way. - Non-deterministic values (like new Date()) are used in SSR. Your current pattern (async composable, conditional/async useAsyncQuery) is likely causing Nuxt to fetch data at different times on server and client, leading to mismatches. 3. Recommended Pattern - Call useAsyncQuery (or useAsyncData) synchronously at the top level of your composable or setup. - Do not wrap it in an async function or call it after an await. - Return the result directly from the composable. #### Example Refactor
// composables/useGroups.js
import { GetGroups as GET_GROUPS } from '@api/graphql/v1/groups.queries.gql'
export function useGroups(organizationId) {
if (!organizationId) {
throw new Error(`Missing organizationId required to initialize composable 'useGroups'.`)
}

// Call useAsyncQuery synchronously
const { data, refresh, error } = useAsyncQuery(GET_GROUPS, { organizationId }, { fetchPolicy: 'network-only' })

// groups is a computed ref based on the query result
const groups = computed(() => data.value?.groups ?? null)

return {
groups,
refetch: refresh,
error
}
}
// composables/useGroups.js
import { GetGroups as GET_GROUPS } from '@api/graphql/v1/groups.queries.gql'
export function useGroups(organizationId) {
if (!organizationId) {
throw new Error(`Missing organizationId required to initialize composable 'useGroups'.`)
}

// Call useAsyncQuery synchronously
const { data, refresh, error } = useAsyncQuery(GET_GROUPS, { organizationId }, { fetchPolicy: 'network-only' })

// groups is a computed ref based on the query result
const groups = computed(() => data.value?.groups ?? null)

return {
groups,
refetch: refresh,
error
}
}
And in your page:
<script setup>
const { user } = await useUser()
const { groups } = useGroups(user.value.organizationId)
</script>
<script setup>
const { user } = await useUser()
const { groups } = useGroups(user.value.organizationId)
</script>
This pattern ensures that the data is fetched during SSR and reused during hydration, preventing mismatches. If you need to refetch, use the refresh method returned by useAsyncQuery. 4. If You Need to Fetch After Mount If you must fetch data only on the client (e.g., after mount), use $fetch inside onMounted, but this will not be SSR-friendly and is not recommended for most SSR pages. Nuxt will warn you if you try to use useAsyncQuery after the component is mounted. --- Summary:
Your hydration mismatch is likely caused by calling useAsyncQuery asynchronously or conditionally. Refactor your composable to call it synchronously at the top level, as shown above, to ensure SSR and hydration work correctly and consistently between server and client. This is the recommended and documented approach in Nuxt 3 for SSR-safe data fetching composables. For more details, see: Nuxt - Component is already mounted, please use $fetch instead and Nuxt Lifecycle and Hydration. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai4w ago
<@1367905110705045598> Kapa.ai is still learning and improving, please let me know how I did by reacting below.

Did you find this page helpful?