Building Reusable Logic with Vue Renderless Components
Renderless components in Vue.js allow you to extract complex UI logic into reusable components without dictating their presentation. This promotes a clean separation of concerns, making your codebase more maintainable and your components more flexible. Your task is to create a set of renderless components that manage common functionalities, demonstrating the power of this pattern.
Problem Description
You are tasked with building two distinct renderless components in Vue.js using TypeScript:
useCounter: A renderless component that manages a simple counter state.useFetch: A renderless component that handles data fetching from a given URL.
These components should expose their state and methods to their parent components via the expose API, allowing the parent to control the rendering and behavior.
Key Requirements:
useCounterComponent:- Should manage a
countproperty, initialized to a providedinitialValue. - Should expose methods to
increment,decrement, andresetthe counter. - The
resetmethod should accept an optionalnewValueto set the counter to.
- Should manage a
useFetchComponent:- Should accept a
urlprop. - Should manage
data(the fetched response),loading(a boolean indicating if fetching is in progress), anderror(any error encountered during fetching). - Should automatically trigger a fetch when the component is mounted and when the
urlprop changes. - Should expose the fetched
data,loadingstate, and anyerrorto the parent. - Should also expose a
refetchmethod to manually re-trigger the fetch.
- Should accept a
Expected Behavior:
- Parent components will use these renderless components via their
setupfunction, utilizingrenderlesspattern principles. - The parent component will then use the exposed state and methods to render its UI and control the logic.
Edge Cases to Consider:
useCounter: Resetting the counter without a new value.useFetch: Handling network errors, empty responses, and the case where theurlprop is initially null or undefined. Ensure that theloadingstate is correctly managed throughout the fetch lifecycle.
Examples
Example 1: useCounter Usage
Parent Component (CounterDisplay.vue)
<script setup lang="ts">
import { ref } from 'vue';
import useCounter from './useCounter'; // Assume this is your renderless component
const { count, increment, decrement, reset } = useCounter(0);
const handleReset = () => {
reset(5); // Reset to 5
};
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
<button @click="reset">Reset</button>
<button @click="handleReset">Reset to 5</button>
</div>
</template>
useCounter.ts (Renderless Component)
import { ref, Ref } from 'vue';
interface UseCounterReturn {
count: Ref<number>;
increment: () => void;
decrement: () => void;
reset: (newValue?: number) => void;
}
export default function useCounter(initialValue: number = 0): UseCounterReturn {
const count = ref<number>(initialValue);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
const reset = (newValue?: number) => {
count.value = newValue !== undefined ? newValue : initialValue;
};
return {
count,
increment,
decrement,
reset,
};
}
Output (when interacting with buttons):
- Initial:
Count: 0 - After clicking "Increment" twice:
Count: 2 - After clicking "Decrement":
Count: 1 - After clicking "Reset":
Count: 0 - After clicking "Reset to 5":
Count: 5
Example 2: useFetch Usage
Parent Component (UserDataFetcher.vue)
<script setup lang="ts">
import { computed } from 'vue';
import useFetch from './useFetch'; // Assume this is your renderless component
const userId = 1; // Example user ID
const userUrl = computed(() => `https://jsonplaceholder.typicode.com/users/${userId}`);
const { data: user, loading, error, refetch } = useFetch(userUrl.value);
</script>
<template>
<div>
<h1>User Data</h1>
<div v-if="loading">Loading user data...</div>
<div v-else-if="error">Error loading user data: {{ error.message }}</div>
<div v-else-if="user">
<p><strong>Name:</strong> {{ user.name }}</p>
<p><strong>Email:</strong> {{ user.email }}</p>
<button @click="refetch">Refetch Data</button>
</div>
<div v-else>No user data available.</div>
</div>
</template>
useFetch.ts (Renderless Component)
import { ref, Ref, watch } from 'vue';
interface User {
id: number;
name: string;
email: string;
// ... other user properties
}
interface UseFetchReturn<T> {
data: Ref<T | null>;
loading: Ref<boolean>;
error: Ref<Error | null>;
refetch: () => Promise<void>;
}
export default function useFetch<T = any>(url: string | null): UseFetchReturn<T> {
const data = ref<T | null>(null);
const loading = ref<boolean>(false);
const error = ref<Error | null>(null);
const fetchData = async () => {
if (!url) {
data.value = null;
return;
}
loading.value = true;
error.value = null;
data.value = null; // Clear previous data
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data.value = await response.json();
} catch (e: any) {
error.value = e;
} finally {
loading.value = false;
}
};
watch(url, () => {
fetchData();
}, { immediate: true }); // Fetch immediately on mount
const refetch = () => {
return fetchData();
};
return {
data,
loading,
error,
refetch,
};
}
Output (assuming successful fetch):
User Data
Name: Leanne Graham
Email: Sincere@april.biz
Example 3: useFetch with Invalid URL
Parent Component (InvalidDataFetcher.vue)
<script setup lang="ts">
import useFetch from './useFetch';
const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/nonexistent-resource');
</script>
<template>
<div>
<h1>Invalid Data Fetch</h1>
<div v-if="loading">Loading...</div>
<div v-else-if="error">An error occurred: {{ error.message }}</div>
<div v-else-if="data">Data fetched successfully (unexpected).</div>
<div v-else>No data and no error.</div>
</div>
</template>
Output:
Invalid Data Fetch
An error occurred: HTTP error! status: 404
Constraints
- Use Vue 3 Composition API with TypeScript.
- Components must be implemented as functions that return an object of exposed properties (composition functions).
- No direct DOM manipulation within the renderless components.
- The
useFetchcomponent must use the nativefetchAPI. - The
watchandcomputedreactivity utilities from Vue can be used within your composition functions.
Notes
- Think about how the parent component will receive and use the values returned by your composition functions. This is the essence of the renderless component pattern.
- For
useFetch, consider how to handle the initial fetch when the component mounts. - The
refetchfunction inuseFetchshould return a Promise to allow the parent component to react to the completion of the refetch if necessary. - The provided examples demonstrate how a parent component would "use" these renderless logic containers. You are building the logic containers themselves.