N
Nuxt6mo ago
RicharDVD

Conditional rendering inside v-for not working properly?

I have the following template code:
<template>
<div class="grid md:grid-cols-3 gap-2">
<div v-for="stat in stats" :key="stat.label" class="bg-white p-6 rounded-md">
<Icon v-if="status === 'pending'" name="heroicons:arrow-path" class="text-gray-500 animate-spin" />
<p v-else class="text-2xl font-bold">
{{ stat.value }}
</p>
<p class="text-gray-500">
{{ stat.label }}
</p>
</div>
</div>
</template>
<template>
<div class="grid md:grid-cols-3 gap-2">
<div v-for="stat in stats" :key="stat.label" class="bg-white p-6 rounded-md">
<Icon v-if="status === 'pending'" name="heroicons:arrow-path" class="text-gray-500 animate-spin" />
<p v-else class="text-2xl font-bold">
{{ stat.value }}
</p>
<p class="text-gray-500">
{{ stat.label }}
</p>
</div>
</div>
</template>
Sometimes when this component is loaded (it seems when I load this page for the first time), both the spinner and the stat value show, even though the status is 'success'. What could cause this?
No description
14 Replies
kapa.ai
kapa.ai6mo ago
Hi @RicharDVD. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- This issue is commonly caused by how your data and status are initialized and updated, especially in a Nuxt/Vue context where reactivity and asynchronous data fetching are involved. From the knowledge sources, a similar problem can occur if the state or data used in a v-for loop is not properly reactive or if the status variable is not updated synchronously with the data. For example, if you use a composable or a shared state that is not properly isolated per component instance, you might see "bleeding" of state or unexpected updates across multiple renders, as described in the "Ref value 'bleeding' over into all v-for instances of component" issue. In that case, the solution was to ensure that each component instance gets its own reactive state by wrapping the composable in a function and calling it per instance, rather than sharing a single ref across all instances Ref value "bleeding" over into all v-for instances of component. Additionally, if your status or data is being set asynchronously (e.g., after a fetch), there could be a brief moment where both the spinner and the value are rendered if the status changes after the data is already available, or vice versa. This can also happen if the initial value of status is not set correctly, or if the template logic does not fully account for all possible states (e.g., pending, success, error). What to check: - Ensure that status is reactive and correctly reflects the fetch lifecycle. If you are using useFetch or useAsyncData, make sure you are using the returned status variable and not a custom one that might be out of sync useFetch: Not immediate. - Make sure that your data (stats) is only set when the status is 'success', and that you do not render the value until the data is actually available. - If you are using a composable or shared state, ensure it is not shared across component instances unless intended. Example fix: You might want to wrap the entire stat value block in a conditional, so only one of the spinner or the value is ever rendered:
<template>
<div class="grid md:grid-cols-3 gap-2">
<div v-for="stat in stats" :key="stat.label" class="bg-white p-6 rounded-md">
<template v-if="status === 'pending'">
<Icon name="heroicons:arrow-path" class="text-gray-500 animate-spin" />
</template>
<template v-else>
<p class="text-2xl font-bold">
{{ stat.value }}
</p>
</template>
<p class="text-gray-500">
{{ stat.label }}
</p>
</div>
</div>
</template>
<template>
<div class="grid md:grid-cols-3 gap-2">
<div v-for="stat in stats" :key="stat.label" class="bg-white p-6 rounded-md">
<template v-if="status === 'pending'">
<Icon name="heroicons:arrow-path" class="text-gray-500 animate-spin" />
</template>
<template v-else>
<p class="text-2xl font-bold">
{{ stat.value }}
</p>
</template>
<p class="text-gray-500">
{{ stat.label }}
</p>
</div>
</div>
</template>
This ensures that only one block is rendered at a time, preventing both the spinner and the value from showing simultaneously. If you are still seeing both, double-check how status is set and updated, and ensure there is no race condition or shared state issue as described above. If you can share how status and stats are defined and updated, a more specific diagnosis could be given. But based on the knowledge sources, improper reactivity or shared state is a likely cause Ref value "bleeding" over into all v-for instances of component. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai6mo ago
<@268039364590829568> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
RicharDVD
RicharDVDOP6mo ago
<script lang="ts" setup>
const { selectedDealership } = useDealership();
const { amountNotSeen } = useNotifications();
const { $api } = useNuxtApp();

const { data, status } = await useAsyncData<{
lead_credits_earned_this_month: number
lead_credits_amount_this_month: number
matches_this_month: number
total_billed_leads: number
total_lead_credits: number
current_lease_requests: number
}>('dealership-stats', () => {
if (selectedDealership.value?.id) {
return $api(`/dealer/dealership/${selectedDealership.value.id}/stats`, {
method: 'GET',
});
} else {
return Promise.resolve(null);
}
}, {
watch: [selectedDealership],
lazy: true,
});

const stats = computed(() => [
{
label: 'Ongeopende meldingen',
value: amountNotSeen.value || 0,
},
{
label: 'Totaal aantal lead credits',
value: data.value?.total_lead_credits || 0,
},
{
label: 'Lopende lease aanvragen',
value: data.value?.current_lease_requests || 0,
},
]);
</script>

<template>
<div class="grid md:grid-cols-3 gap-2">
<div v-for="stat in stats" :key="stat.label" class="bg-white p-6 rounded-md">
<template v-if="status === 'pending'">
<Icon name="heroicons:arrow-path" class="text-gray-500 animate-spin" />
</template>
<template v-else>
<p class="text-2xl font-bold">
{{ stat.value }}
</p>
</template>
<p class="text-gray-500">
{{ stat.label }}
</p>
</div>
</div>
</template>
<script lang="ts" setup>
const { selectedDealership } = useDealership();
const { amountNotSeen } = useNotifications();
const { $api } = useNuxtApp();

const { data, status } = await useAsyncData<{
lead_credits_earned_this_month: number
lead_credits_amount_this_month: number
matches_this_month: number
total_billed_leads: number
total_lead_credits: number
current_lease_requests: number
}>('dealership-stats', () => {
if (selectedDealership.value?.id) {
return $api(`/dealer/dealership/${selectedDealership.value.id}/stats`, {
method: 'GET',
});
} else {
return Promise.resolve(null);
}
}, {
watch: [selectedDealership],
lazy: true,
});

const stats = computed(() => [
{
label: 'Ongeopende meldingen',
value: amountNotSeen.value || 0,
},
{
label: 'Totaal aantal lead credits',
value: data.value?.total_lead_credits || 0,
},
{
label: 'Lopende lease aanvragen',
value: data.value?.current_lease_requests || 0,
},
]);
</script>

<template>
<div class="grid md:grid-cols-3 gap-2">
<div v-for="stat in stats" :key="stat.label" class="bg-white p-6 rounded-md">
<template v-if="status === 'pending'">
<Icon name="heroicons:arrow-path" class="text-gray-500 animate-spin" />
</template>
<template v-else>
<p class="text-2xl font-bold">
{{ stat.value }}
</p>
</template>
<p class="text-gray-500">
{{ stat.label }}
</p>
</div>
</div>
</template>
This is what the entire component looks like @kapa.ai
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Thank you for providing the full component code. Based on your code and the knowledge sources, here are the most likely reasons why both the spinner and the stat value sometimes show at the same time, even though status is 'success': 1. Reactivity and Timing of status and data - The status returned by useAsyncData is reactive and should accurately reflect the fetch state. However, if you are using the lazy: true option, navigation will not be blocked and the component will render before the data is loaded, requiring you to handle the loading state manually. This is the correct approach, but it means that the initial render may briefly show default values (like 0) for your stats, even while the status is still 'pending' or just switched to 'success' Docs: useFetch lazy. 2. Computed stats Array - Your stats array is computed from both amountNotSeen and data.value. If data.value is initially null or undefined, your computed will use the fallback 0 for those stats. If the template renders while status is still 'pending', the spinner will show. But if status changes to 'success' before data.value is updated, or if the computed property updates out of sync with the status, you could see both the spinner and the value momentarily. 3. Shared or Stale State - If useNotifications or useDealership composables are sharing state across component instances (not likely in your code, but possible if not implemented as a function), you could see "bleeding" of state, as described in this Stack Overflow answer. Make sure your composables return new refs per call if they are meant to be instance-specific. 4. Nuxt/AsyncData Status Alignment - There was a change in Nuxt 3 where the pending property is now only true when status is also 'pending'. If you rely on pending, ensure your logic is updated to use status for more accurate control Upgrade Guide: Alignment of pending value. --- How to Fix Ensure that only one of the spinner or the value is ever rendered: Your template already uses mutually exclusive <template v-if="status === 'pending'"> and <template v-else>, which is correct. However, if you still see both, it suggests that either: - The status is not updating in sync with data.value, or - The computed stats is updating before the status changes, causing a brief moment where both are rendered. Try this adjustment: - Instead of using v-else, use v-else-if="status === 'success'" to be explicit. - Optionally, add a fallback for other statuses (like 'error' or 'idle').
<template>
<div class="grid md:grid-cols-3 gap-2">
<div v-for="stat in stats" :key="stat.label" class="bg-white p-6 rounded-md">
<template v-if="status === 'pending'">
<Icon name="heroicons:arrow-path" class="text-gray-500 animate-spin" />
</template>
<template v-else-if="status === 'success'">
<p class="text-2xl font-bold">
{{ stat.value }}
</p>
</template>
<template v-else>
<p class="text-2xl font-bold text-gray-400">
-
</p>
</template>
<p class="text-gray-500">
{{ stat.label }}
</p>
</div>
</div>
</template>
<template>
<div class="grid md:grid-cols-3 gap-2">
<div v-for="stat in stats" :key="stat.label" class="bg-white p-6 rounded-md">
<template v-if="status === 'pending'">
<Icon name="heroicons:arrow-path" class="text-gray-500 animate-spin" />
</template>
<template v-else-if="status === 'success'">
<p class="text-2xl font-bold">
{{ stat.value }}
</p>
</template>
<template v-else>
<p class="text-2xl font-bold text-gray-400">
-
</p>
</template>
<p class="text-gray-500">
{{ stat.label }}
</p>
</div>
</div>
</template>
This ensures that only one block is rendered for each status, and avoids any overlap. --- Summary:
The issue is likely due to a race condition between the computed property and the status update, or a brief moment where the default values are rendered before the data is loaded. Using explicit status checks in your template, as shown above, should resolve the issue. If the problem persists, double-check your composables for shared state issues as described in the knowledge sources Ref value "bleeding" over into all v-for instances of component, and ensure you are using the latest Nuxt version with the correct status/pending alignment Upgrade Guide: Alignment of pending value. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai6mo ago
<@268039364590829568> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
RicharDVD
RicharDVDOP6mo ago
@kapa.ai Replacing the v-else with v-else-if="status === 'succes'" doesn't work either. I still see both loading spinner and the value. I can see in devtools that status is indeed 'success' so I don't understand why the loading spinner is still showing.
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Based on the knowledge sources, if you see both the loading spinner and the value even when status is 'success', and you have confirmed in devtools that status is indeed 'success', this suggests that the issue is not with the status value itself, but likely with how the conditional rendering is being handled in your template or with the reactivity of the data used in your v-for. A similar issue is described in the knowledge sources where a shared or improperly scoped reactive value (such as a ref defined outside of a function or composable) can "bleed" across component instances, causing unexpected rendering behavior. The solution in that case was to ensure that each instance of the component or composable has its own isolated reactive state by wrapping the state in a function and calling it per instance, rather than sharing a single ref across all instances Ref value "bleeding" over into all v-for instances of component. However, your code does not show any obvious shared state issues, and your use of status in the template is correct. The knowledge sources do not provide a direct explanation for why both the spinner and the value would render when status is 'success'. In summary:
I'm sorry, but the knowledge sources do not contain enough information to explain why both the loading spinner and the value are rendered when status is 'success', especially if you have confirmed the value of status in devtools and your template uses mutually exclusive conditions. If you suspect a Nuxt or Vue rendering bug, you may want to check for any custom CSS or slot content that could be affecting visibility, or try to reproduce the issue in a minimal example for further debugging. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai6mo ago
<@268039364590829568> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
RicharDVD
RicharDVDOP6mo ago
@kapa.ai I notices that the issue might be caused by the logic inside the useAsyncData I use. selectedDealership.value is only populated on the client and in some cases is initially zero. It than calls the useAsyncData again once the selectedDealership is populated because of the watcher. If I remove this logic and replace it with a simple promise that resolves an object after 2 seconds (for test purposes), it seems to work. Do you now maybe know what causes this?
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Yes, your observation aligns with known issues in Nuxt's data fetching and reactivity system, especially when using useAsyncData with a dependency (watch) that is initially unset or only available on the client. When selectedDealership.value is initially undefined or 0, your useAsyncData handler returns Promise.resolve(null). Later, when selectedDealership is populated (due to client-side hydration or some async process), the watcher triggers useAsyncData to re-run. This can cause a brief state where the component renders with incomplete or default data, and then re-renders with the actual data, potentially leading to both the loading spinner and the value being visible at the same time. This pattern is discussed in the knowledge sources, where issues arise if the data used in useAsyncData is not available on the server or is only set on the client. The watcher will trigger a re-fetch, and depending on how your template and computed properties are set up, you may see a "flash" of both states or unexpected rendering behavior. This is especially true if the initial state is not handled carefully, or if the data is not available during SSR and only becomes available after hydration, causing a mismatch between server and client renders and possible hydration errors Rendering of layouts/*.vue files is not blocked when using useAsyncData. Key points: - If your data dependency (selectedDealership) is only available on the client, useAsyncData will run once with null (or default) and again when the value is set, causing two fetch cycles and potentially two different UI states. - This can lead to hydration issues or UI flashes, as described in the knowledge sources. - Using a static promise (as in your test) avoids this because the data is always available and does not depend on a reactive value that changes after hydration. Recommendation:
To avoid this, ensure that your data dependencies are available before calling useAsyncData, or handle the initial state explicitly in your template to prevent rendering incomplete data. You may also want to guard your fetch logic so it only runs when the required value is present, or use a more robust pattern for client-only data fetching. This behavior is a known limitation and source of confusion in Nuxt's SSR/client hydration model Rendering of layouts/*.vue files is not blocked when using useAsyncData. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai6mo ago
<@268039364590829568> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
RicharDVD
RicharDVDOP6mo ago
@kapa.ai I managed to fix the issue by putting a div around the Icon component and putting the v-if on there. It seems there was an issue with the v-if on the Icon component but I'm not sure what caused that. It looked like the loading state did indeed work but there was always an extra loading spinner. One that worked correctly and one that showed always. So during the loading state, I saw 2 loading spinners. Do you know what might have caused this?
kapa.ai
kapa.ai6mo ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- Based on the knowledge sources, there is no explicit documentation or discussion about <Icon> components in Nuxt or Nuxt UI always rendering an extra instance when used with v-if. However, similar issues can occur in Vue/Nuxt when: - A component (like <Icon>) is always rendered due to its own internal logic, slot usage, or how it is imported/registered. - There is a mismatch between the template logic and the actual DOM output, especially if the component is not handling conditional rendering as expected. In your case, wrapping the <Icon> in a <div> and applying v-if to the <div> resolved the issue, which suggests that the <Icon> component itself might not have been fully removed from the DOM when using v-if directly on it. This could be due to how the component is implemented or how Vue handles conditional rendering for certain components. The knowledge sources do not provide a direct explanation for this specific behavior with the <Icon> component. If this is a third-party or custom component, it may have internal logic that causes it to render even when v-if is used, or there could be a slot or default content that always renders. Summary:
I'm sorry, but the knowledge sources do not contain enough information to explain exactly why using v-if directly on the <Icon> component caused an extra spinner to always show. Wrapping it in a <div> and applying v-if there is a valid workaround, and this pattern is sometimes necessary with certain components in Vue/Nuxt when direct conditional rendering does not behave as expected. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai6mo ago
<@268039364590829568> Kapa.ai is still learning and improving, please let me know how I did by reacting below.

Did you find this page helpful?