Composables
useWithLoading
Wrap async functions with automatic loading state management while preserving their exact signature.
The useWithLoading composable wraps any function with a reactive isLoading state that automatically tracks the function's execution. It preserves the exact argument and return types of the original function, making it type-safe and flexible.
Basic Usage
const sendEmail = async (email: string) => {
await $fetch('/api/send-email', {
method: 'POST',
body: { email }
})
}
const { isLoading, fnWithLoading: sendEmailWithLoading } = useWithLoading(sendEmail)
// Call the wrapped function - isLoading will be true during execution
await sendEmailWithLoading('user@example.com')
components/ReviewForm.vue
<script setup lang="ts">
const rating = ref(5)
const comment = ref('')
const submitReview = async (reviewData: { rating: number; comment: string }) => {
await $fetch('/api/reviews', {
method: 'POST',
body: reviewData
})
}
const { isLoading, fnWithLoading: postReview } = useWithLoading(submitReview)
async function handleSubmit() {
await postReview({ rating: rating.value, comment: comment.value })
comment.value = ''
}
</script>
<template>
<div>
<UFormGroup label="Rating">
<UInput
v-model="rating"
type="number"
:disabled="isLoading"
/>
</UFormGroup>
<UFormGroup label="Comment">
<UTextarea
v-model="comment"
:disabled="isLoading"
placeholder="Share your thoughts..."
/>
</UFormGroup>
<!-- Loading indicator in form area -->
<div v-if="isLoading" class="my-4 flex items-center gap-2">
<UIcon name="i-heroicons-arrow-path" class="animate-spin" />
<span class="text-sm text-gray-600">Submitting your review...</span>
</div>
<!-- Multiple buttons disabled during submission -->
<div class="mt-4 flex gap-2">
<UButton @click="handleSubmit" :disabled="isLoading">
Submit Review
</UButton>
<UButton color="gray" :disabled="isLoading">
Save Draft
</UButton>
<UButton color="gray" variant="ghost" :disabled="isLoading">
Cancel
</UButton>
</div>
</div>
</template>
Parameters
The composable accepts a single function parameter:
| Parameter | Type | Description |
|---|---|---|
fn | (...args: Args) => R | Promise<R> | The function to wrap. Can be sync or async. All argument types and return types are preserved |
Return Value
Returns an object with:
isLoading- ReactiveRef<boolean>that'strueduring function executionfnWithLoading- Wrapped function with identical signature that manages loading state
Examples
Multiple Parameters
const updateUser = async (userId: string, name: string, age: number) => {
return await $fetch(`/api/users/${userId}`, {
method: 'PATCH',
body: { name, age },
})
}
const { isLoading, fnWithLoading: updateUserWithLoading } = useWithLoading(updateUser)
// Type-safe: all parameters required with correct types
await updateUserWithLoading('123', 'Ada Lovelace', 36)
Return Value Handling
const fetchUserData = async (userId: string) => {
const data = await $fetch<User>(`/api/users/${userId}`)
return data
}
const { isLoading, fnWithLoading: getUserData } = useWithLoading(fetchUserData)
// Return type is preserved
const userData = await getUserData('123') // Type: User
Multiple Loading States
const {
isLoading: isSaving,
fnWithLoading: saveData
} = useWithLoading(async (data: FormData) => {
await $fetch('/api/save', { method: 'POST', body: data })
})
const {
isLoading: isDeleting,
fnWithLoading: deleteData
} = useWithLoading(async (id: string) => {
await $fetch(`/api/items/${id}`, { method: 'DELETE' })
})
<template>
<div>
<UButton :loading="isSaving" @click="saveData(formData)">
Save
</UButton>
<UButton :loading="isDeleting" @click="deleteData('123')">
Delete
</UButton>
</div>
</template>
Type Safety
The composable uses TypeScript generics to preserve the exact signature:
- Arguments: All parameter types are maintained
- Return type: Original return type is preserved (wrapped in
Promiseif not already) - Autocomplete: Full IDE support for both parameters and return values

