How-To: Managing Loading States with useLoading
¶
The useLoading
composable provides an elegant solution for combining multiple loading states in VC-Shell applications. This guide demonstrates how to effectively manage complex loading scenarios where multiple asynchronous operations need to be tracked simultaneously.
Prerequisites¶
- Understanding of Vue 3 Composition API, including
ref
andcomputed
. - Familiarity with the
useLoading
composable (see useLoading API Reference). - Basic knowledge of the
useAsync
composable for asynchronous operations. - Understanding of reactive state management in Vue applications.
Core Concept¶
The useLoading
composable takes multiple boolean reactive references and returns a computed property that indicates whether any of those states are currently true
. This pattern is essential for:
- Unified Loading Indicators: Show a single loading state while multiple operations are in progress
- UI State Management: Disable forms or buttons during any ongoing operation
- User Experience: Provide clear feedback when multiple background tasks are running
- Performance Optimization: Avoid redundant API calls while operations are pending
import { useLoading } from '@vc-shell/framework';
const isLoading = useLoading(loadingState1, loadingState2, loadingState3);
// isLoading.value will be true if ANY of the individual states are true
Implementation Strategies¶
1. Basic Data Loading Coordination¶
Coordinate multiple data fetching operations for a complete page load:
<!-- DashboardPage.vue -->
<template>
<div class="dashboard-page">
<VcLoading v-if="isLoadingData" overlay />
<div v-else class="dashboard-content">
<div class="stats-section">
<StatsWidget :data="statsData" />
</div>
<div class="charts-section">
<SalesChart :data="salesData" />
<OrdersChart :data="ordersData" />
</div>
<div class="tables-section">
<RecentOrders :orders="recentOrders" />
<TopProducts :products="topProducts" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useAsync, useLoading, VcLoading } from '@vc-shell/framework';
// Individual data states
const statsData = ref(null);
const salesData = ref([]);
const ordersData = ref([]);
const recentOrders = ref([]);
const topProducts = ref([]);
// Create async operations for each data source
const { loading: loadingStats, action: fetchStats } = useAsync(async () => {
const response = await api.getStats();
statsData.value = response;
});
const { loading: loadingSales, action: fetchSales } = useAsync(async () => {
const response = await api.getSalesData();
salesData.value = response;
});
const { loading: loadingOrders, action: fetchOrdersData } = useAsync(async () => {
const response = await api.getOrdersData();
ordersData.value = response;
});
const { loading: loadingRecentOrders, action: fetchRecentOrders } = useAsync(async () => {
const response = await api.getRecentOrders();
recentOrders.value = response;
});
const { loading: loadingTopProducts, action: fetchTopProducts } = useAsync(async () => {
const response = await api.getTopProducts();
topProducts.value = response;
});
// Combine all loading states
const isLoadingData = useLoading(
loadingStats,
loadingSales,
loadingOrders,
loadingRecentOrders,
loadingTopProducts
);
onMounted(async () => {
// Start all data fetching operations
await Promise.all([
fetchStats(),
fetchSales(),
fetchOrdersData(),
fetchRecentOrders(),
fetchTopProducts()
]);
});
</script>
2. Form Operations with Multiple Actions¶
Manage loading states for forms with multiple possible actions:
<!-- ProductForm.vue -->
<template>
<form @submit.prevent="handleSubmit">
<div class="form-fields">
<VcInput
v-model="product.name"
label="Product Name"
:disabled="isAnyOperationRunning"
/>
<VcInput
v-model="product.price"
label="Price"
type="number"
:disabled="isAnyOperationRunning"
/>
<VcTextarea
v-model="product.description"
label="Description"
:disabled="isAnyOperationRunning"
/>
</div>
<div class="form-actions">
<VcButton
type="submit"
:loading="isSaving"
:disabled="isAnyOperationRunning"
>
Save Product
</VcButton>
<VcButton
variant="outline"
:loading="isValidating"
:disabled="isAnyOperationRunning"
@click="validateProduct"
>
Validate
</VcButton>
<VcButton
variant="outline"
:loading="isPublishing"
:disabled="isAnyOperationRunning"
@click="publishProduct"
>
Save & Publish
</VcButton>
<VcButton
variant="danger"
:loading="isDeleting"
:disabled="isAnyOperationRunning"
@click="deleteProduct"
>
Delete
</VcButton>
</div>
<div v-if="isAnyOperationRunning" class="operation-status">
<VcLoading size="sm" />
<span>Processing...</span>
</div>
</form>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { useAsync, useLoading, VcButton, VcInput, VcTextarea, VcLoading } from '@vc-shell/framework';
interface Props {
productId?: string;
}
const props = defineProps<Props>();
const product = ref({
name: '',
price: 0,
description: ''
});
// Individual operation states
const { loading: isSaving, action: saveProduct } = useAsync(async () => {
await api.saveProduct(product.value);
notification.success('Product saved successfully');
});
const { loading: isValidating, action: validateProduct } = useAsync(async () => {
const errors = await api.validateProduct(product.value);
if (errors.length === 0) {
notification.success('Product validation passed');
} else {
notification.error('Product validation failed');
}
});
const { loading: isPublishing, action: publishProduct } = useAsync(async () => {
await api.saveProduct(product.value);
await api.publishProduct(product.value.id);
notification.success('Product saved and published');
});
const { loading: isDeleting, action: deleteProduct } = useAsync(async () => {
await api.deleteProduct(props.productId);
notification.success('Product deleted');
router.push('/products');
});
// Combine all operation states
const isAnyOperationRunning = useLoading(
isSaving,
isValidating,
isPublishing,
isDeleting
);
function handleSubmit() {
saveProduct();
}
</script>
3. Hierarchical Loading States¶
Create different loading contexts for different parts of your application:
<!-- ProductManagement.vue -->
<template>
<div class="product-management">
<!-- Header with global actions -->
<div class="header">
<h1>Product Management</h1>
<VcButton
:loading="isImporting"
:disabled="isAnyGlobalOperation"
@click="importProducts"
>
Import Products
</VcButton>
<VcButton
:loading="isExporting"
:disabled="isAnyGlobalOperation"
@click="exportProducts"
>
Export Products
</VcButton>
</div>
<!-- Search and filters -->
<div class="filters" :class="{ 'tw-opacity-50': isLoadingData }">
<VcInput
v-model="searchQuery"
placeholder="Search products..."
:disabled="isLoadingData"
@input="searchProducts"
/>
<VcSelect
v-model="selectedCategory"
:options="categories"
:disabled="isLoadingData"
@change="filterProducts"
/>
</div>
<!-- Product list -->
<div class="product-list">
<VcLoading v-if="isLoadingData" />
<VcTable
v-else
:items="products"
:loading="isBulkOperating"
@bulk-action="handleBulkAction"
>
<template #actions="{ item }">
<VcButton
size="sm"
:loading="isUpdatingProduct(item.id)"
:disabled="isAnyProductOperation"
@click="updateProduct(item)"
>
Update
</VcButton>
<VcButton
size="sm"
variant="danger"
:loading="isDeletingProduct(item.id)"
:disabled="isAnyProductOperation"
@click="deleteProduct(item.id)"
>
Delete
</VcButton>
</template>
</VcTable>
</div>
<!-- Global loading overlay for major operations -->
<VcLoading v-if="isAnyGlobalOperation" overlay />
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { useAsync, useLoading } from '@vc-shell/framework';
const searchQuery = ref('');
const selectedCategory = ref('');
const products = ref([]);
const categories = ref([]);
// Data loading operations
const { loading: loadingProducts, action: fetchProducts } = useAsync(async () => {
const response = await api.getProducts({ search: searchQuery.value, category: selectedCategory.value });
products.value = response;
});
const { loading: loadingCategories, action: fetchCategories } = useAsync(async () => {
const response = await api.getCategories();
categories.value = response;
});
// Global operations
const { loading: isImporting, action: importProducts } = useAsync(async () => {
await api.importProducts();
await fetchProducts(); // Refresh data
});
const { loading: isExporting, action: exportProducts } = useAsync(async () => {
await api.exportProducts();
});
// Product-specific operations
const { loading: isBulkOperating, action: handleBulkAction } = useAsync(async (action, items) => {
await api.bulkAction(action, items);
await fetchProducts(); // Refresh data
});
const updatingProducts = ref(new Set());
const deletingProducts = ref(new Set());
// Individual product operations
async function updateProduct(product) {
updatingProducts.value.add(product.id);
try {
await api.updateProduct(product);
await fetchProducts();
} finally {
updatingProducts.value.delete(product.id);
}
}
async function deleteProduct(productId) {
deletingProducts.value.add(productId);
try {
await api.deleteProduct(productId);
await fetchProducts();
} finally {
deletingProducts.value.delete(productId);
}
}
// Computed loading states for different contexts
const isLoadingData = useLoading(loadingProducts, loadingCategories);
const isAnyGlobalOperation = useLoading(isImporting, isExporting);
const isAnyProductOperation = computed(() =>
isBulkOperating.value || updatingProducts.value.size > 0 || deletingProducts.value.size > 0
);
// Helper functions for individual product states
const isUpdatingProduct = (productId) => updatingProducts.value.has(productId);
const isDeletingProduct = (productId) => deletingProducts.value.has(productId);
// Search and filter functions
const { loading: isSearching, action: searchProducts } = useAsync(async () => {
await fetchProducts();
});
const { loading: isFiltering, action: filterProducts } = useAsync(async () => {
await fetchProducts();
});
</script>
4. API Client Integration¶
Combine useLoading
with API clients for comprehensive state management:
// useProductOperations.ts
import { useApiClient, useAsync, useLoading } from '@vc-shell/framework';
import { ProductsClient } from '@your-api-package';
export function useProductOperations() {
const { getApiClient } = useApiClient(ProductsClient);
// Individual operations
const { loading: isCreating, action: createProduct } = useAsync(async (productData) => {
const client = await getApiClient();
return await client.createProduct(productData);
});
const { loading: isUpdating, action: updateProduct } = useAsync(async (product) => {
const client = await getApiClient();
return await client.updateProduct(product);
});
const { loading: isDeleting, action: deleteProduct } = useAsync(async (productId) => {
const client = await getApiClient();
return await client.deleteProduct(productId);
});
const { loading: isValidating, action: validateProduct } = useAsync(async (product) => {
const client = await getApiClient();
return await client.validateProduct(product);
});
const { loading: isPublishing, action: publishProduct } = useAsync(async (productId) => {
const client = await getApiClient();
return await client.publishProduct(productId);
});
// Combined loading states for different contexts
const isMutating = useLoading(isCreating, isUpdating, isDeleting);
const isProcessing = useLoading(isValidating, isPublishing);
const isAnyOperation = useLoading(isCreating, isUpdating, isDeleting, isValidating, isPublishing);
return {
// Individual operations
createProduct,
updateProduct,
deleteProduct,
validateProduct,
publishProduct,
// Individual loading states
isCreating,
isUpdating,
isDeleting,
isValidating,
isPublishing,
// Combined loading states
isMutating,
isProcessing,
isAnyOperation
};
}
5. Complex Workflow Management¶
Handle multi-step workflows with dependent operations:
// useProductWorkflow.ts
import { useAsync, useLoading } from '@vc-shell/framework';
export function useProductWorkflow() {
// Step 1: Validation
const { loading: isValidating, action: validateProduct } = useAsync(async (product) => {
const errors = await api.validateProduct(product);
if (errors.length > 0) {
throw new Error('Validation failed');
}
return true;
});
// Step 2: Save draft
const { loading: isSavingDraft, action: saveDraft } = useAsync(async (product) => {
return await api.saveProductDraft(product);
});
// Step 3: Upload images
const { loading: isUploadingImages, action: uploadImages } = useAsync(async (productId, images) => {
return await api.uploadProductImages(productId, images);
});
// Step 4: Final publication
const { loading: isPublishing, action: publishProduct } = useAsync(async (productId) => {
return await api.publishProduct(productId);
});
// Combined states for different workflow phases
const isPreparingProduct = useLoading(isValidating, isSavingDraft);
const isFinalizingProduct = useLoading(isUploadingImages, isPublishing);
const isWorkflowRunning = useLoading(isValidating, isSavingDraft, isUploadingImages, isPublishing);
// Complete workflow function
const { loading: isRunningWorkflow, action: runCompleteWorkflow } = useAsync(async (product, images) => {
// Step 1: Validate
await validateProduct(product);
// Step 2: Save draft
const savedProduct = await saveDraft(product);
// Step 3: Upload images
if (images && images.length > 0) {
await uploadImages(savedProduct.id, images);
}
// Step 4: Publish
await publishProduct(savedProduct.id);
return savedProduct;
});
return {
// Individual steps
validateProduct,
saveDraft,
uploadImages,
publishProduct,
// Complete workflow
runCompleteWorkflow,
// Loading states
isValidating,
isSavingDraft,
isUploadingImages,
isPublishing,
isRunningWorkflow,
// Combined states
isPreparingProduct,
isFinalizingProduct,
isWorkflowRunning
};
}
Best Practices¶
-
Logical Grouping: Group related loading states together based on their UI impact and user context.
-
Granular Control: Create multiple combined loading states for different parts of your UI rather than one global state.
-
User Experience: Use loading states to provide appropriate feedback and disable relevant UI elements.
-
Performance: Avoid unnecessary re-renders by being selective about which loading states to combine.
-
Error Handling: Remember that loading states don't handle errors - combine with proper error handling patterns.
-
Accessibility: Ensure loading states are properly announced to screen readers and provide meaningful feedback.
-
Testing: Test loading state combinations to ensure they behave correctly in all scenarios.
Integration with Other Composables¶
With useAsync¶
// Perfect combination for managing async operations
const { loading: loadingData, action: fetchData } = useAsync(async () => {
// Async operation
});
const { loading: savingData, action: saveData } = useAsync(async (data) => {
// Save operation
});
const isAnyOperation = useLoading(loadingData, savingData);
With API Clients¶
// Combine with useApiClient for comprehensive state management
const { getApiClient } = useApiClient(MyApiClient);
const { loading: isLoading, action: performOperation } = useAsync(async () => {
const client = await getApiClient();
return await client.someOperation();
});
By following these patterns and best practices, you can create sophisticated loading state management that provides excellent user experience while maintaining clean and maintainable code.