diff --git a/code/websites/pokedex.online/src/composables/useAsyncState.js b/code/websites/pokedex.online/src/composables/useAsyncState.js new file mode 100644 index 0000000..99d9aff --- /dev/null +++ b/code/websites/pokedex.online/src/composables/useAsyncState.js @@ -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} 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 + }; +}