Advanced

Optimistic Updates & Custom Validation

Enhance UX with instant feedback and custom validation rules

Provide instant user feedback with optimistic updates and implement custom validation logic.

Optimistic Updates

Mutations automatically invalidate cache and refetch data. However, you can add optimistic updates for instant feedback:

<script setup lang="ts">
const { items } = await useCollectionQuery('shopProducts')
const { update } = useCollectionMutation('shopProducts')

const toggleFeatured = async (product: Product) => {
  // Optimistic: Update UI immediately
  const index = items.value.findIndex(p => p.id === product.id)
  if (index !== -1) {
    items.value[index].featured = !items.value[index].featured
  }

  try {
    // API call
    await update(product.id, {
      featured: !product.featured
    })
    // Cache auto-refetches on success
  } catch (error) {
    // Rollback on error
    if (index !== -1) {
      items.value[index].featured = !items.value[index].featured
    }
  }
}
</script>

How Optimistic Updates Work

  1. Immediate Update: Modify local state before the API call
  2. API Call: Make the actual mutation request
  3. Auto-Refetch: On success, cache automatically refreshes
  4. Rollback: On error, revert the local state change

Custom Validation

Implement custom validation rules using Zod schemas with cross-field and async validation:

// composables/useProducts.ts
const schema = z.object({
  name: z.string().min(1),
  price: z.number().min(0),
  discountPrice: z.number().optional()
})
.refine((data) => {
  // Cross-field validation
  if (data.discountPrice && data.discountPrice >= data.price) {
    return false
  }
  return true
}, {
  message: 'Discount must be less than price',
  path: ['discountPrice']
})
.refine(async (data) => {
  // Async validation
  const exists = await $fetch(`/api/products/check-name?name=${data.name}`)
  return !exists
}, {
  message: 'Product name already exists'
})

Validation Types

Cross-Field Validation

Validate relationships between multiple fields:

.refine((data) => {
  if (data.discountPrice && data.discountPrice >= data.price) {
    return false
  }
  return true
}, {
  message: 'Discount must be less than price',
  path: ['discountPrice']
})

Async Validation

Perform asynchronous checks like database queries:

.refine(async (data) => {
  const exists = await $fetch(`/api/products/check-name?name=${data.name}`)
  return !exists
}, {
  message: 'Product name already exists'
})

Best Practices

Optimistic Updates

  • Always implement error rollback
  • Use for frequent, low-risk operations (toggles, favorites)
  • Avoid for critical operations (payments, deletions)
  • Provide visual feedback during the operation

Custom Validation

  • Keep validation logic close to your forms
  • Use clear, user-friendly error messages
  • Specify the path to attach errors to specific fields
  • Debounce async validation to reduce API calls
  • Cache async validation results when possible