✨ Add composable for managing asynchronous state
This commit is contained in:
130
code/websites/pokedex.online/src/composables/useAsyncState.js
Normal file
130
code/websites/pokedex.online/src/composables/useAsyncState.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* useAsyncState Composable
|
||||
*
|
||||
* Manages loading/error/success states for async operations
|
||||
* Consolidates pattern used across 13+ components
|
||||
*
|
||||
* @example
|
||||
* const { execute, loading, error, data, isSuccess } = useAsyncState();
|
||||
* await execute(async () => {
|
||||
* return await fetchSomeData();
|
||||
* });
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export function useAsyncState(options = {}) {
|
||||
const {
|
||||
initialData = null,
|
||||
onSuccess = null,
|
||||
onError = null,
|
||||
maxRetries = 0,
|
||||
retryDelay = 1000
|
||||
} = options;
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const data = ref(initialData);
|
||||
const abortController = ref(null);
|
||||
|
||||
const isSuccess = computed(() => !loading.value && !error.value && data.value !== null);
|
||||
const isError = computed(() => !loading.value && error.value !== null);
|
||||
const isIdle = computed(() => !loading.value && error.value === null && data.value === null);
|
||||
|
||||
/**
|
||||
* Execute an async function with state management
|
||||
* @param {Function} asyncFn - Async function to execute
|
||||
* @param {Object} executeOptions - Options for this execution
|
||||
* @returns {Promise<any>} Result of async function
|
||||
*/
|
||||
async function execute(asyncFn, executeOptions = {}) {
|
||||
const { retries = maxRetries, signal = null } = executeOptions;
|
||||
|
||||
// Create abort controller if not provided
|
||||
if (!signal) {
|
||||
abortController.value = new AbortController();
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
let lastError;
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt <= retries) {
|
||||
try {
|
||||
const result = await asyncFn(signal || abortController.value?.signal);
|
||||
data.value = result;
|
||||
loading.value = false;
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
|
||||
// Don't retry if aborted
|
||||
if (err.name === 'AbortError') {
|
||||
break;
|
||||
}
|
||||
|
||||
attempt++;
|
||||
|
||||
// If more retries remaining, wait before retrying
|
||||
if (attempt <= retries) {
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All retries exhausted
|
||||
error.value = lastError;
|
||||
loading.value = false;
|
||||
|
||||
if (onError) {
|
||||
onError(lastError);
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current async operation
|
||||
*/
|
||||
function cancel() {
|
||||
if (abortController.value) {
|
||||
abortController.value.abort();
|
||||
abortController.value = null;
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset state to initial values
|
||||
*/
|
||||
function reset() {
|
||||
cancel();
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
data.value = initialData;
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
|
||||
// Computed
|
||||
isSuccess,
|
||||
isError,
|
||||
isIdle,
|
||||
|
||||
// Methods
|
||||
execute,
|
||||
cancel,
|
||||
reset
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user