vuejs_state_composables 28 Q&As

Vue.js State Composables FAQ & Answers

28 expert Vue.js State Composables answers researched from official documentation. Every answer cites authoritative sources you can verify.

unknown

28 questions
A

Create composable function with reactive state defined outside setup(). Pattern: export const useState = () => { const state = reactive({ user: null, isAuthenticated: false }); const login = (user) => { state.user = user; state.isAuthenticated = true; }; return { state: readonly(state), login }; }. Use readonly() to prevent external mutations. Import in components: const { state, login } = useState();. Singleton pattern: define state outside function for shared instance across all components. Best practices: (1) Use Pinia for complex apps (official recommendation 2025), (2) Composables for simple global state (<5 properties), (3) Separate global from local state, (4) Create modular stores per feature (useAuth, useCart, useTheme), (5) Use provide/inject for scoped state (not app-wide). Avoid one giant global context, prefer multiple specialized contexts. TypeScript: interface State { user: User | null; }. For non-singleton instances use composables, for singleton use Pinia.

99% confidence
A

Use CSS custom properties with reactive theme state. Pattern: create useTheme composable: const theme = ref<'light' | 'dark'>('light'); const setTheme = (newTheme) => { theme.value = newTheme; document.documentElement.setAttribute('data-theme', newTheme); }; return { theme, setTheme };. CSS: [data-theme='light'] { --bg-color: #fff; --text-color: #000; } [data-theme='dark'] { --bg-color: #1a1a1a; --text-color: #fff; }. Components use CSS variables: .button { background: var(--bg-color); color: var(--text-color); }. Persist theme: watchEffect(() => { localStorage.setItem('theme', theme.value); }); onMounted(() => { const saved = localStorage.getItem('theme'); if (saved) setTheme(saved); });. Detect system preference: const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches. Tailwind alternative: use dark: prefix classes with dark mode config. TypeScript types: type Theme = 'light' | 'dark' | 'auto'. Best practice: provide theme at root, inject in components needing theme-specific logic.

99% confidence
A

Composables solve critical mixin problems. Benefits: (1) Clear source: import { useCounter } from './composables' shows exact origin vs mixins where property source is unclear, (2) No namespace collisions: composables use explicit imports, rename on conflict (import { count as userCount }), mixins merge properties causing conflicts, (3) Better TypeScript: composables provide natural type inference (standard functions/variables), mixins have poor type support, (4) Modular reusability: compose multiple composables cleanly, mixins create brittle inheritance chains, (5) Explicit dependencies: see all used composables in import statements, mixins hide dependencies, (6) Flexible naming: destructure and rename returned values, mixins force specific property names. Example: const { count, increment } = useCounter(); const { count: todoCount } = useTodos(); // Rename to avoid collision. 2025 recommendation: Composition API with composables is the standard for modern, scalable Vue apps. Mixins remain only for legacy Options API code. Migration path: convert mixins to composables when refactoring.

99% confidence
A

DRY (Don't Repeat Yourself) principle: every piece of knowledge should have single, unambiguous representation in codebase. In Vue: (1) Extract reusable components: instead of duplicating user display markup, (2) Create composables for shared logic: useAuth(), useValidation(), usePagination() instead of copying functions, (3) Centralize constants: export const API_URL = '...' instead of hardcoding URLs, (4) Use utility functions: formatDate(), debounce(), truncate() in utils/, (5) Shared types: interface User {} defined once in types/, (6) CSS variables: --primary-color used throughout vs repeated #3498db. Anti-pattern: copying code with 'TODO: refactor'. Benefits: (1) Single source of truth (bug fix in one place), (2) Easier maintenance, (3) Consistent behavior. Balance: avoid over-abstraction, don't DRY code that will diverge. Rule of three: duplicate twice, extract on third occurrence. TypeScript helps enforce DRY with shared types/interfaces. Best practice: composables + components + utilities = DRY Vue architecture.

99% confidence
A

SOLID principles adapted for Vue: (S) Single Responsibility: component does one thing, e.g., displays user, doesn't handle auth/routing/API, (I) Interface Segregation: props should be minimal, don't pass entire object when only need ID, use slots for flexibility, (D) Dependency Inversion: inject dependencies (API services) rather than hardcode, use provide/inject or composables. Example SRP violation: rendering profile + handling payments + managing settings. Fix: split into , , . Interface Segregation: interface ButtonProps { variant: string; size: string; } not { ...allPossibleProps }. Dependency Inversion: const api = inject('api'); api.getUser() vs hardcoded fetch('/api/user'). Benefits: testable (mock dependencies), maintainable (small focused components), reusable. Note: (L) Liskov Substitution, (O) Open/Closed less applicable to Vue. Best practice: composables implement SRP (useAuth doesn't handle routing), components compose multiple focused composables.

99% confidence
A

Follow these patterns for reusable composables: (1) Start with 'use' prefix: useAuth, useCart, usePagination (convention), (2) Return object with clear API: return { data, loading, error, refresh } not loose variables, (3) Accept configuration options: useApi({ baseURL, timeout, retries }), (4) Use TypeScript generics: useFetch('/api/user'), (5) Provide cleanup: use onUnmounted for event listeners/timers, (6) Separate concerns: useAuth handles auth only, not routing/API/storage, (7) Make stateless when possible: pure functions easier to test. Example structure: export function useCounter(initial = 0) { const count = ref(initial); const increment = () => count.value++; return { count: readonly(count), increment }; }. Advanced: composable composition: const { user } = useAuth(); const { cart } = useCart(user.value.id);. Avoid: global state in composables (use Pinia instead), side effects in returned values, mixing local and global state. Test composables independently with @vue/test-utils mountComposable.

99% confidence
A

Use local reactive state within modal with props for initial values. Pattern: const formData = reactive({ ...props.initialData }); avoids mutating props directly. Reset on close: const resetForm = () => { Object.assign(formData, props.initialData); }; watch(isOpen, (open) => { if (!open) resetForm(); });. Validation: use VeeValidate or custom composable: const { errors, validate } = useValidation(formData);. Submit: const handleSubmit = async () => { if (await validate()) { emit('save', toRaw(formData)); close(); } };. Prevent accidental close: const confirmClose = () => { if (hasUnsavedChanges.value) { openDialog(ConfirmDialog, { message: 'Discard changes?' }).then(close); } else close(); };. Best practices: (1) Clone initial data (avoid reference mutations), (2) Use toRaw() before emitting (removes reactivity proxies), (3) Disable submit during loading, (4) Focus first input on mount, (5) Handle Enter/Escape keys. TypeScript: const formData = reactive({ name: '', email: '' });. Alternative: use FormKit for complex forms with built-in validation/state management.

99% confidence
A

Update UI immediately, rollback on error. Pattern with TanStack Query (Vue Query): const { mutate } = useMutation({ mutationFn: updateTodo, onMutate: async (newTodo) => { await queryClient.cancelQueries(['todos']); const previous = queryClient.getQueryData(['todos']); queryClient.setQueryData(['todos'], old => [...old, newTodo]); return { previous }; }, onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context.previous); }, onSuccess: () => { queryClient.invalidateQueries(['todos']); } }); mutate(newTodo);. Manual implementation: const optimisticUpdate = async (item) => { items.value.push(item); // Optimistic UI try { const saved = await api.save(item); items.value = items.value.map(i => i.id === item.id ? saved : i); } catch (error) { items.value = items.value.filter(i => i.id !== item.id); // Rollback showError('Save failed'); } };. Benefits: instant feedback, better perceived performance. Risks: complexity, possible rollback jarring UX. Best for: high-success-rate operations (likes, toggles), not critical data (payments). Use loading indicators during server sync.

99% confidence
A

Use VueUse's useDebounceFn or useDebounce for reactive debouncing. Function debounce: import { useDebounceFn } from '@vueuse/core'; const debouncedSearch = useDebounceFn(async (query) => { results.value = await api.search(query); }, 500); // 500ms delay. Usage: <input @input="debouncedSearch($event.target.value)" />. Value debounce: const searchQuery = ref(''); const debouncedQuery = useDebounce(searchQuery, 500); watch(debouncedQuery, async (value) => { results.value = await api.search(value); });. Options: useDebounceFn(fn, 500, { maxWait: 2000 }). Manual implementation: let timeoutId; const debounce = (fn, delay) => (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; const debouncedFn = debounce(search, 500);. Cleanup: onUnmounted(() => clearTimeout(timeoutId));. Use cases: search input (500ms), window resize (200ms), autosave (1000ms). Combine with loading state: const isSearching = ref(false). VueUse watchDebounced: automatic watch + debounce.

99% confidence
A

Use reactive boolean flags with consistent patterns. Simple pattern: const isLoading = ref(false); const fetchData = async () => { isLoading.value = true; try { data.value = await api.get(); } finally { isLoading.value = false; } };. Template:

Loading...
{{ data }}
. Multiple states: const state = ref<'idle' | 'loading' | 'success' | 'error'>('idle');. Advanced composable: const useAsync = (fn) => { const data = ref(); const error = ref(); const isLoading = ref(false); const execute = async (...args) => { isLoading.value = true; error.value = null; try { data.value = await fn(...args); } catch (e) { error.value = e; } finally { isLoading.value = false; } }; return { data, error, isLoading, execute }; }. Usage: const { data, isLoading, execute } = useAsync(api.getUser); onMounted(() => execute(userId));. Global loading: use provide/inject for app-wide spinner. Best practice: (1) Always use finally for cleanup, (2) Disable buttons during loading, (3) Show skeleton screens for better UX, (4) Use TanStack Query for automatic loading state management.

99% confidence
A

Use centralized error handling with user-friendly messages. Pattern: create error interceptor in API client: axios.interceptors.response.use(response => response, error => { if (error.response?.status === 401) { router.push('/login'); } else if (error.response?.status === 403) { showError('Permission denied'); } else if (error.response?.status >= 500) { showError('Server error. Please try again.'); } else { showError(error.response?.data?.message || 'An error occurred'); } return Promise.reject(error); });. Component level: const { data, error, isLoading } = useAsync(api.getUser); if (error.value) { showError(error.value.message); }. Global error handler: app.config.errorHandler = (err) => { console.error(err); Sentry.captureException(err); };. User-facing errors: use toast notifications (vue-toastification), not console.log. TypeScript: class ApiError extends Error { constructor(public status: number, message: string) { super(message); } }. Best practices: (1) Log errors to monitoring service (Sentry, LogRocket), (2) Show actionable messages ('Retry' button), (3) Handle offline state, (4) Use error boundaries for component errors.

99% confidence
A

Use exponential backoff with retry limit. Pattern with axios-retry: npm install axios-retry; import axiosRetry from 'axios-retry'; axiosRetry(axios, { retries: 3, retryDelay: (retryCount) => retryCount * 1000, retryCondition: (error) => axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response?.status >= 500 }); // Retries: 1s, 2s, 3s. Manual implementation: const fetchWithRetry = async (fn, maxRetries = 3) => { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; const delay = Math.min(1000 * 2 ** i, 10000); // Exponential: 1s, 2s, 4s, max 10s await new Promise(resolve => setTimeout(resolve, delay)); } } };. Usage: const data = await fetchWithRetry(() => api.getUser()); TanStack Query built-in: useQuery({ queryKey: ['user'], queryFn: getUser, retry: 3, retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000) });. Best practices: (1) Only retry idempotent requests (GET, not POST), (2) Retry network errors and 5xx, not 4xx, (3) Show retry attempt to user, (4) Add jitter to prevent thundering herd.

99% confidence
A

Use computed() function for cached reactive derivations. Basic: import { computed } from 'vue'; const count = ref(0); const double = computed(() => count.value * 2); // Auto-updates when count changes. Writable computed: const firstName = ref('John'); const lastName = ref('Doe'); const fullName = computed({ get: () => ${firstName.value} ${lastName.value}, set: (value) => { [firstName.value, lastName.value] = value.split(' '); } }); fullName.value = 'Jane Smith'; // Updates firstName and lastName. Benefits vs methods: (1) Cached: only recalculates when dependencies change, methods run on every render, (2) Reactive: automatically tracks dependencies, (3) Lazy: doesn't evaluate until accessed. Multiple dependencies: const summary = computed(() => ${user.value.name} has ${items.value.length} items);. TypeScript: const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0));. Best practices: (1) Use for derived state, not methods, (2) Keep getters pure (no side effects), (3) Don't mutate reactive state in computed, (4) Use watch() for side effects.

99% confidence
A

Use ref() for primitives, reactive() for objects in Composition API. ref: const count = ref(0); count.value++; // Access with .value. Auto-unwrapped in templates: {{ count }}. reactive: const state = reactive({ count: 0, user: null }); state.count++; // No .value needed. Choose ref for: primitives (string, number, boolean), need reassignment (user.value = newUser). Choose reactive for: objects/arrays, prefer dot notation (state.user.name). Deep reactivity: nested properties automatically reactive. Shallow: shallowRef/shallowReactive for performance. toRefs: convert reactive object to refs: const { count, user } = toRefs(state); // count.value, user.value. readonly: prevent mutations: const readonlyState = readonly(state);. Best practices: (1) Pinia for global state (2025 official recommendation), (2) Local state with ref/reactive in components, (3) Consistent pattern: all ref or all reactive per component, (4) Use TypeScript interfaces for reactive objects: const state = reactive({ ... });. Watch reactive changes: watch(() => state.count, (newVal) => { /* ... */ }).

99% confidence
A

Create composable function with reactive state defined outside setup(). Pattern: export const useState = () => { const state = reactive({ user: null, isAuthenticated: false }); const login = (user) => { state.user = user; state.isAuthenticated = true; }; return { state: readonly(state), login }; }. Use readonly() to prevent external mutations. Import in components: const { state, login } = useState();. Singleton pattern: define state outside function for shared instance across all components. Best practices: (1) Use Pinia for complex apps (official recommendation 2025), (2) Composables for simple global state (<5 properties), (3) Separate global from local state, (4) Create modular stores per feature (useAuth, useCart, useTheme), (5) Use provide/inject for scoped state (not app-wide). Avoid one giant global context, prefer multiple specialized contexts. TypeScript: interface State { user: User | null; }. For non-singleton instances use composables, for singleton use Pinia.

99% confidence
A

Use CSS custom properties with reactive theme state. Pattern: create useTheme composable: const theme = ref<'light' | 'dark'>('light'); const setTheme = (newTheme) => { theme.value = newTheme; document.documentElement.setAttribute('data-theme', newTheme); }; return { theme, setTheme };. CSS: [data-theme='light'] { --bg-color: #fff; --text-color: #000; } [data-theme='dark'] { --bg-color: #1a1a1a; --text-color: #fff; }. Components use CSS variables: .button { background: var(--bg-color); color: var(--text-color); }. Persist theme: watchEffect(() => { localStorage.setItem('theme', theme.value); }); onMounted(() => { const saved = localStorage.getItem('theme'); if (saved) setTheme(saved); });. Detect system preference: const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches. Tailwind alternative: use dark: prefix classes with dark mode config. TypeScript types: type Theme = 'light' | 'dark' | 'auto'. Best practice: provide theme at root, inject in components needing theme-specific logic.

99% confidence
A

Composables solve critical mixin problems. Benefits: (1) Clear source: import { useCounter } from './composables' shows exact origin vs mixins where property source is unclear, (2) No namespace collisions: composables use explicit imports, rename on conflict (import { count as userCount }), mixins merge properties causing conflicts, (3) Better TypeScript: composables provide natural type inference (standard functions/variables), mixins have poor type support, (4) Modular reusability: compose multiple composables cleanly, mixins create brittle inheritance chains, (5) Explicit dependencies: see all used composables in import statements, mixins hide dependencies, (6) Flexible naming: destructure and rename returned values, mixins force specific property names. Example: const { count, increment } = useCounter(); const { count: todoCount } = useTodos(); // Rename to avoid collision. 2025 recommendation: Composition API with composables is the standard for modern, scalable Vue apps. Mixins remain only for legacy Options API code. Migration path: convert mixins to composables when refactoring.

99% confidence
A

DRY (Don't Repeat Yourself) principle: every piece of knowledge should have single, unambiguous representation in codebase. In Vue: (1) Extract reusable components: instead of duplicating user display markup, (2) Create composables for shared logic: useAuth(), useValidation(), usePagination() instead of copying functions, (3) Centralize constants: export const API_URL = '...' instead of hardcoding URLs, (4) Use utility functions: formatDate(), debounce(), truncate() in utils/, (5) Shared types: interface User {} defined once in types/, (6) CSS variables: --primary-color used throughout vs repeated #3498db. Anti-pattern: copying code with 'TODO: refactor'. Benefits: (1) Single source of truth (bug fix in one place), (2) Easier maintenance, (3) Consistent behavior. Balance: avoid over-abstraction, don't DRY code that will diverge. Rule of three: duplicate twice, extract on third occurrence. TypeScript helps enforce DRY with shared types/interfaces. Best practice: composables + components + utilities = DRY Vue architecture.

99% confidence
A

SOLID principles adapted for Vue: (S) Single Responsibility: component does one thing, e.g., displays user, doesn't handle auth/routing/API, (I) Interface Segregation: props should be minimal, don't pass entire object when only need ID, use slots for flexibility, (D) Dependency Inversion: inject dependencies (API services) rather than hardcode, use provide/inject or composables. Example SRP violation: rendering profile + handling payments + managing settings. Fix: split into , , . Interface Segregation: interface ButtonProps { variant: string; size: string; } not { ...allPossibleProps }. Dependency Inversion: const api = inject('api'); api.getUser() vs hardcoded fetch('/api/user'). Benefits: testable (mock dependencies), maintainable (small focused components), reusable. Note: (L) Liskov Substitution, (O) Open/Closed less applicable to Vue. Best practice: composables implement SRP (useAuth doesn't handle routing), components compose multiple focused composables.

99% confidence
A

Follow these patterns for reusable composables: (1) Start with 'use' prefix: useAuth, useCart, usePagination (convention), (2) Return object with clear API: return { data, loading, error, refresh } not loose variables, (3) Accept configuration options: useApi({ baseURL, timeout, retries }), (4) Use TypeScript generics: useFetch('/api/user'), (5) Provide cleanup: use onUnmounted for event listeners/timers, (6) Separate concerns: useAuth handles auth only, not routing/API/storage, (7) Make stateless when possible: pure functions easier to test. Example structure: export function useCounter(initial = 0) { const count = ref(initial); const increment = () => count.value++; return { count: readonly(count), increment }; }. Advanced: composable composition: const { user } = useAuth(); const { cart } = useCart(user.value.id);. Avoid: global state in composables (use Pinia instead), side effects in returned values, mixing local and global state. Test composables independently with @vue/test-utils mountComposable.

99% confidence
A

Use local reactive state within modal with props for initial values. Pattern: const formData = reactive({ ...props.initialData }); avoids mutating props directly. Reset on close: const resetForm = () => { Object.assign(formData, props.initialData); }; watch(isOpen, (open) => { if (!open) resetForm(); });. Validation: use VeeValidate or custom composable: const { errors, validate } = useValidation(formData);. Submit: const handleSubmit = async () => { if (await validate()) { emit('save', toRaw(formData)); close(); } };. Prevent accidental close: const confirmClose = () => { if (hasUnsavedChanges.value) { openDialog(ConfirmDialog, { message: 'Discard changes?' }).then(close); } else close(); };. Best practices: (1) Clone initial data (avoid reference mutations), (2) Use toRaw() before emitting (removes reactivity proxies), (3) Disable submit during loading, (4) Focus first input on mount, (5) Handle Enter/Escape keys. TypeScript: const formData = reactive({ name: '', email: '' });. Alternative: use FormKit for complex forms with built-in validation/state management.

99% confidence
A

Update UI immediately, rollback on error. Pattern with TanStack Query (Vue Query): const { mutate } = useMutation({ mutationFn: updateTodo, onMutate: async (newTodo) => { await queryClient.cancelQueries(['todos']); const previous = queryClient.getQueryData(['todos']); queryClient.setQueryData(['todos'], old => [...old, newTodo]); return { previous }; }, onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context.previous); }, onSuccess: () => { queryClient.invalidateQueries(['todos']); } }); mutate(newTodo);. Manual implementation: const optimisticUpdate = async (item) => { items.value.push(item); // Optimistic UI try { const saved = await api.save(item); items.value = items.value.map(i => i.id === item.id ? saved : i); } catch (error) { items.value = items.value.filter(i => i.id !== item.id); // Rollback showError('Save failed'); } };. Benefits: instant feedback, better perceived performance. Risks: complexity, possible rollback jarring UX. Best for: high-success-rate operations (likes, toggles), not critical data (payments). Use loading indicators during server sync.

99% confidence
A

Use VueUse's useDebounceFn or useDebounce for reactive debouncing. Function debounce: import { useDebounceFn } from '@vueuse/core'; const debouncedSearch = useDebounceFn(async (query) => { results.value = await api.search(query); }, 500); // 500ms delay. Usage: <input @input="debouncedSearch($event.target.value)" />. Value debounce: const searchQuery = ref(''); const debouncedQuery = useDebounce(searchQuery, 500); watch(debouncedQuery, async (value) => { results.value = await api.search(value); });. Options: useDebounceFn(fn, 500, { maxWait: 2000 }). Manual implementation: let timeoutId; const debounce = (fn, delay) => (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; const debouncedFn = debounce(search, 500);. Cleanup: onUnmounted(() => clearTimeout(timeoutId));. Use cases: search input (500ms), window resize (200ms), autosave (1000ms). Combine with loading state: const isSearching = ref(false). VueUse watchDebounced: automatic watch + debounce.

99% confidence
A

Use reactive boolean flags with consistent patterns. Simple pattern: const isLoading = ref(false); const fetchData = async () => { isLoading.value = true; try { data.value = await api.get(); } finally { isLoading.value = false; } };. Template:

Loading...
{{ data }}
. Multiple states: const state = ref<'idle' | 'loading' | 'success' | 'error'>('idle');. Advanced composable: const useAsync = (fn) => { const data = ref(); const error = ref(); const isLoading = ref(false); const execute = async (...args) => { isLoading.value = true; error.value = null; try { data.value = await fn(...args); } catch (e) { error.value = e; } finally { isLoading.value = false; } }; return { data, error, isLoading, execute }; }. Usage: const { data, isLoading, execute } = useAsync(api.getUser); onMounted(() => execute(userId));. Global loading: use provide/inject for app-wide spinner. Best practice: (1) Always use finally for cleanup, (2) Disable buttons during loading, (3) Show skeleton screens for better UX, (4) Use TanStack Query for automatic loading state management.

99% confidence
A

Use centralized error handling with user-friendly messages. Pattern: create error interceptor in API client: axios.interceptors.response.use(response => response, error => { if (error.response?.status === 401) { router.push('/login'); } else if (error.response?.status === 403) { showError('Permission denied'); } else if (error.response?.status >= 500) { showError('Server error. Please try again.'); } else { showError(error.response?.data?.message || 'An error occurred'); } return Promise.reject(error); });. Component level: const { data, error, isLoading } = useAsync(api.getUser); if (error.value) { showError(error.value.message); }. Global error handler: app.config.errorHandler = (err) => { console.error(err); Sentry.captureException(err); };. User-facing errors: use toast notifications (vue-toastification), not console.log. TypeScript: class ApiError extends Error { constructor(public status: number, message: string) { super(message); } }. Best practices: (1) Log errors to monitoring service (Sentry, LogRocket), (2) Show actionable messages ('Retry' button), (3) Handle offline state, (4) Use error boundaries for component errors.

99% confidence
A

Use exponential backoff with retry limit. Pattern with axios-retry: npm install axios-retry; import axiosRetry from 'axios-retry'; axiosRetry(axios, { retries: 3, retryDelay: (retryCount) => retryCount * 1000, retryCondition: (error) => axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response?.status >= 500 }); // Retries: 1s, 2s, 3s. Manual implementation: const fetchWithRetry = async (fn, maxRetries = 3) => { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; const delay = Math.min(1000 * 2 ** i, 10000); // Exponential: 1s, 2s, 4s, max 10s await new Promise(resolve => setTimeout(resolve, delay)); } } };. Usage: const data = await fetchWithRetry(() => api.getUser()); TanStack Query built-in: useQuery({ queryKey: ['user'], queryFn: getUser, retry: 3, retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000) });. Best practices: (1) Only retry idempotent requests (GET, not POST), (2) Retry network errors and 5xx, not 4xx, (3) Show retry attempt to user, (4) Add jitter to prevent thundering herd.

99% confidence
A

Use computed() function for cached reactive derivations. Basic: import { computed } from 'vue'; const count = ref(0); const double = computed(() => count.value * 2); // Auto-updates when count changes. Writable computed: const firstName = ref('John'); const lastName = ref('Doe'); const fullName = computed({ get: () => ${firstName.value} ${lastName.value}, set: (value) => { [firstName.value, lastName.value] = value.split(' '); } }); fullName.value = 'Jane Smith'; // Updates firstName and lastName. Benefits vs methods: (1) Cached: only recalculates when dependencies change, methods run on every render, (2) Reactive: automatically tracks dependencies, (3) Lazy: doesn't evaluate until accessed. Multiple dependencies: const summary = computed(() => ${user.value.name} has ${items.value.length} items);. TypeScript: const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0));. Best practices: (1) Use for derived state, not methods, (2) Keep getters pure (no side effects), (3) Don't mutate reactive state in computed, (4) Use watch() for side effects.

99% confidence
A

Use ref() for primitives, reactive() for objects in Composition API. ref: const count = ref(0); count.value++; // Access with .value. Auto-unwrapped in templates: {{ count }}. reactive: const state = reactive({ count: 0, user: null }); state.count++; // No .value needed. Choose ref for: primitives (string, number, boolean), need reassignment (user.value = newUser). Choose reactive for: objects/arrays, prefer dot notation (state.user.name). Deep reactivity: nested properties automatically reactive. Shallow: shallowRef/shallowReactive for performance. toRefs: convert reactive object to refs: const { count, user } = toRefs(state); // count.value, user.value. readonly: prevent mutations: const readonlyState = readonly(state);. Best practices: (1) Pinia for global state (2025 official recommendation), (2) Local state with ref/reactive in components, (3) Consistent pattern: all ref or all reactive per component, (4) Use TypeScript interfaces for reactive objects: const state = reactive({ ... });. Watch reactive changes: watch(() => state.count, (newVal) => { /* ... */ }).

99% confidence