Hone logo
Hone
Problems

Implementing a Suspense Component in Vue.js (TypeScript)

Vue 3 introduced the <Suspense> component, enabling developers to gracefully handle asynchronous operations within their components. This challenge asks you to recreate a simplified version of this mechanism to manage loading states for asynchronous data fetching. This is crucial for improving user experience by preventing UI flickering and providing clear feedback during data retrieval.

Problem Description

Your task is to create a custom Suspense component in Vue.js using TypeScript. This component will wrap other components that perform asynchronous operations (like fetching data). The Suspense component should display a designated "fallback" UI while the wrapped component is resolving its asynchronous task. Once the asynchronous task is complete and the wrapped component is ready, the Suspense component should seamlessly switch to rendering the wrapped component.

Key Requirements:

  • Component Wrapping: The Suspense component should accept a default slot where the asynchronous component will be placed.
  • Fallback Slot: The Suspense component should accept a named slot called fallback to display content while the default slot is pending.
  • Asynchronous Resolution: The Suspense component needs to detect when an asynchronous operation within its default slot component has completed. For this challenge, we'll assume asynchronous operations are signaled by a component returning a Promise from its setup function or a method called within the setup (e.g., an async function that returns data).
  • State Management: The Suspense component must internally manage its state: pending (displaying fallback) and resolved (displaying default slot).
  • Dynamic Updates: If the asynchronous component's data changes and triggers a new asynchronous operation, the Suspense should re-enter the pending state.
  • TypeScript Support: The solution must be implemented in TypeScript, ensuring type safety.

Expected Behavior:

  1. When Suspense is mounted, it initially renders the fallback content.
  2. It waits for the default slot component to signal completion of its asynchronous operation.
  3. Once the operation is complete, it unmounts the fallback and mounts the default slot component.
  4. If the default slot component triggers another asynchronous operation, the fallback should be shown again until the new operation completes.

Edge Cases to Consider:

  • What happens if the asynchronous operation in the default slot component throws an error? (For this challenge, you can choose to display an error message or simply not render anything. A more robust implementation would handle this.)
  • What if the default slot component has no asynchronous operations? The Suspense should immediately render the default slot.

Examples

Example 1: Basic Usage with Data Fetching

Consider a UserProfile component that fetches user data asynchronously.

Input:

A Vue component structure like this:

<template>
  <Suspense>
    <template #fallback>
      <div>Loading user profile...</div>
    </template>
    <UserProfile :userId="1" />
  </Suspense>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import UserProfile from './UserProfile.vue'; // Assume UserProfile fetches data

export default defineComponent({
  components: {
    UserProfile,
  },
});
</script>

And UserProfile.vue:

<template>
  <div>
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
</template>

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

interface User {
  id: number;
  name: string;
  email: string;
}

export default defineComponent({
  props: {
    userId: {
      type: Number,
      required: true,
    },
  },
  async setup(props) {
    const user = ref<User | null>(null);

    const fetchUser = async (id: number): Promise<User> => {
      // Simulate network delay
      await new Promise(resolve => setTimeout(resolve, 1000));
      // In a real app, this would be an API call
      return {
        id,
        name: `User ${id}`,
        email: `user${id}@example.com`,
      };
    };

    user.value = await fetchUser(props.userId);

    return { user };
  },
});
</script>

Output:

Initially, the text "Loading user profile..." is displayed. After 1 second, it's replaced by:

<div>
  <h2>User 1</h2>
  <p>user1@example.com</p>
</div>

Explanation:

The Suspense component detects that UserProfile's setup function returns a Promise. It renders the fallback content until the Promise resolves. Once resolved, it renders the UserProfile component.

Example 2: Handling Re-fetching

Imagine the UserProfile component's userId prop changes.

Input:

The same structure as Example 1, but the parent component updates the userId:

<template>
  <Suspense>
    <template #fallback>
      <div>Loading user profile...</div>
    </template>
    <UserProfile :userId="currentUserId" />
  </Suspense>
  <button @click="changeUser">Load Next User</button>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import UserProfile from './UserProfile.vue';

export default defineComponent({
  components: {
    UserProfile,
  },
  setup() {
    const currentUserId = ref(1);

    const changeUser = () => {
      currentUserId.value++;
    };

    return { currentUserId, changeUser };
  },
});
</script>

Output:

When the "Load Next User" button is clicked:

  1. The "Loading user profile..." fallback reappears.
  2. After the delay, the content updates to display "User 2" and "user2@example.com".

Explanation:

When currentUserId changes, the UserProfile component re-renders and its setup function is re-executed, returning a new Promise. The Suspense component detects this new asynchronous operation and re-enters the pending state, showing the fallback before rendering the updated UserProfile.

Example 3: Component with No Async Operation

Input:

<template>
  <Suspense>
    <template #fallback>
      <div>This should not be shown if no async op.</div>
    </template>
    <SimpleComponent message="Hello!" />
  </Suspense>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import SimpleComponent from './SimpleComponent.vue'; // Assume SimpleComponent has no async op

export default defineComponent({
  components: {
    SimpleComponent,
  },
});
</script>

SimpleComponent.vue:

<template>
  <div>{{ message }}</div>
</template>

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

export default defineComponent({
  props: {
    message: {
      type: String,
      required: true,
    },
  },
  setup(props) {
    // No Promise returned
    return { message: props.message };
  },
});
</script>

Output:

The text "Hello!" is displayed immediately. The fallback is never shown.

Explanation:

Since SimpleComponent's setup function does not return a Promise, the Suspense component immediately renders the default slot content.

Constraints

  • You must use Vue 3 and TypeScript.
  • The Suspense component should be a functional component or a setup-based component.
  • Your Suspense implementation should be able to handle components that return a Promise from their setup function or from an async function called directly within setup.
  • The fallback slot must be named #fallback.
  • The challenge focuses on the core suspense mechanism; advanced error handling within the suspense component itself is not required for a basic solution.

Notes

  • Think about how Vue's reactivity system and the setup function's return value can be leveraged.
  • You will need to use Vue's <slot> and <keep-alive> concepts (though <keep-alive> is not strictly required for the core suspense logic itself, it's related to component lifecycle management).
  • Consider how you might detect if a component's setup function is indeed asynchronous. The simplest way for this challenge is to check if the return value is a Promise.
  • The goal is to simulate the behavior of Vue's built-in <Suspense> component, not to replicate all its intricacies.
Loading editor...
typescript