Vue Suspense: Building a Staggered Content Reveal
This challenge focuses on implementing Vue's Suspense component to create a visually engaging experience by staggering the loading and rendering of multiple asynchronous content pieces. This technique is crucial for improving perceived performance and guiding user attention, making applications feel more dynamic and responsive.
Problem Description
You are tasked with building a Vue 3 component that displays several independent pieces of asynchronous content, such as data fetched from different APIs or simulated network requests. The goal is to use Vue's Suspense and v-for directive to reveal this content sequentially with a small delay between each item. This creates a "suspenseful" effect, where elements fade or slide into view one after another.
Key Requirements:
- Asynchronous Content: Each content item should simulate an asynchronous operation (e.g., using
setTimeoutand Promises). SuspenseIntegration: Wrap the dynamically rendered content within a<Suspense>component.- Sequential Reveal: Implement a mechanism to introduce a delay between the rendering of each content item.
- Loading State: Display a clear "Loading..." indicator while any of the asynchronous operations are pending.
- Rendered Content: Once all asynchronous operations are complete, display the actual content for each item.
Expected Behavior:
When the component mounts:
- A "Loading..." message should be displayed.
- After a short initial delay (e.g., 200ms), the first content item will start to resolve.
- Each subsequent content item will resolve and render approximately 500ms after the previous one.
- Once all content items have resolved, they will all be visible.
Edge Cases to Consider:
- What happens if an asynchronous operation fails? (For this challenge, we will assume successful operations).
- Handling a large number of items: While not heavily performance-tested, the solution should be reasonably efficient.
Examples
Example 1:
Input: A list of 3 items, each with a simulated 1-second asynchronous fetch.
Component Structure:
<template>
<Suspense>
<template #default>
<div class="content-container">
<div v-for="(item, index) in items" :key="item.id" class="content-item" :style="{ transitionDelay: `${index * 0.5}s` }">
{{ item.data }}
</div>
</div>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue';
interface Item {
id: number;
data: string;
}
export default defineComponent({
async setup() {
const items = ref<Item[]>([]);
const fetchData = async (id: number): Promise<Item> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, data: `Content for item ${id}` });
}, 1000); // Simulate 1-second fetch
});
};
onMounted(async () => {
const fetchedItems: Item[] = [];
for (let i = 1; i <= 3; i++) {
const item = await fetchData(i);
fetchedItems.push(item);
items.value = [...fetchedItems]; // Update reactively to trigger Suspense for each item
}
});
return { items };
}
});
</script>
<style>
.content-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.content-item {
padding: 15px;
border: 1px solid #ccc;
background-color: #f9f9f9;
opacity: 0; /* Initially hidden */
animation: fadeIn 0.5s forwards; /* Fade-in animation */
}
/* This animation relies on the transitionDelay applied inline */
@keyframes fadeIn {
to {
opacity: 1;
}
}
</style>
Output:
Initially, "Loading..." is displayed. After 1 second, "Content for item 1" appears. After another 0.5 seconds (1.5s total), "Content for item 2" appears. Finally, after another 0.5 seconds (2s total), "Content for item 3" appears. Each item fades in sequentially.
Explanation:
The Suspense component shows the fallback until all promises returned by the setup function are resolved. In this example, onMounted simulates fetching data for each item. By updating items.value within the loop, we trigger re-renders that Suspense will wait for. The transitionDelay and CSS animation are applied to the individual content items to create the staggered reveal effect.
Example 2:
Input: A list of 2 items, with one taking significantly longer to fetch.
Component Structure: (Same as Example 1, but with adjusted setTimeout durations)
// Inside setup() function:
const fetchData = async (id: number): Promise<Item> => {
return new Promise((resolve) => {
const delay = id === 1 ? 500 : 2000; // Item 2 takes longer
setTimeout(() => {
resolve({ id, data: `Content for item ${id}` });
}, delay);
});
};
// ... rest of the component remains similar
Output:
"Loading..." is displayed. After 0.5 seconds, "Content for item 1" appears. "Content for item 2" will only appear after its 2-second fetch is complete, approximately 2 seconds after the component mounted. The staggered reveal will still occur, with item 2 appearing after item 1, but the total load time is dictated by the longest individual fetch.
Explanation:
Suspense waits for all promises to resolve. Even though item 1 finishes early, the overall suspense is not lifted until item 2's promise resolves. The staggered visual reveal is still controlled by the transitionDelay, but the timing of when each item becomes available is dictated by its individual asynchronous operation.
Constraints
- The solution must be implemented in Vue 3 with TypeScript.
- Use the
<Suspense>component for managing asynchronous rendering. - Simulate asynchronous operations using
PromiseandsetTimeout. - Each asynchronous operation should take at least 200ms to complete.
- The staggered reveal delay between consecutive items should be at least 300ms.
- The initial fallback content should be displayed until at least one item begins to resolve.
Notes
- Consider how
Suspensehandles multiple promises. It waits for all promises within its default slot to resolve. - To achieve the staggered effect, you'll need to apply delays to the rendering of individual elements after they have been fetched. CSS transitions or animations are excellent for this.
- Think about how to update your reactive state (
reforreactive) within the asynchronous fetching process to ensureSuspensecorrectly tracks the completion of each item. Updating the array reactively as each item resolves is a common pattern.