Skip to content

Instantly share code, notes, and snippets.

@productdevbook
Last active July 19, 2025 07:14
Show Gist options
  • Save productdevbook/3fb64654f3093cd386b84c192f46dcf0 to your computer and use it in GitHub Desktop.
Save productdevbook/3fb64654f3093cd386b84c192f46dcf0 to your computer and use it in GitHub Desktop.
Pinia Colada: The Magical Way of Fetching Data in Vue.js 🍹

Pinia Colada: The Magical Way of Fetching Data in Vue.js 🍹

What is Pinia Colada? Learning Through an E-commerce Story

Imagine an online store. This store has thousands of products, and customers are constantly viewing products, adding them to cart, and making purchases. Pinia Colada is an amazing tool for managing this store's data!

Pinia Colada is a smart data fetching layer developed for Vue.js. The name might remind you of a cocktail, but it's actually a tool built on top of the Pinia state management library that makes data fetching operations much easier.

What's the Difference Between Pinia and Pinia Colada?

Let's tell a little story to understand this:

Pinia is like a warehouse - Your store's warehouse. It's where you store your products, customer information, and order records. But in this warehouse, you only do storage.

Pinia Colada is like a smart shipping system - It automatically organizes products coming to your warehouse, quickly fetches them when customers request, fetches the same product only once when multiple people want it (this is called deduplication), and most importantly, keeps frequently requested products ready in advance (this is called caching).

<template>
  <div class="smart-product-hover">
    <h3>🎯 Smart Product Cards with Cache Optimization</h3>
    
    <div class="product-grid">
      <div 
        v-for="product in allProducts.data" 
        :key="product.id"
        class="product-card"
        @mouseenter="handleMouseEnter(product.id)"
        @click="navigateToProduct(product.id)"
      >
        <img :src="product.image" :alt="product.name" />
        <h4>{{ product.name }}</h4>
        <p class="price">${{ product.price }}</p>
        
        <!-- Show cache status -->
        <div class="cache-indicator">
          <span v-if="isInCache(product.id)" class="cached">
            ⚑ Cached
          </span>
          <span v-else class="not-cached">
            πŸ“‘ Will load
          </span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { useQueryCache } from '@pinia/colada'
import { useRouter } from 'vue-router'
import { useProducts } from '@/composables/useProducts'

const router = useRouter()
const queryCache = useQueryCache()
const { allProducts, preloadProductFromList, loadProductInBackground } = useProducts()

function handleMouseEnter(productId) {
  // Strategy 1: If we have basic info, put it in cache immediately
  preloadProductFromList(productId)
  
  // Strategy 2: Start background loading for full details
  loadProductInBackground(productId)
}

function isInCache(productId) {
  return !!queryCache.getQueryData(['product-details', productId])
}

function navigateToProduct(productId) {
  router.push(`/product/${productId}`)
  // User will see instant loading because data is already cached!
}
</script>

Advanced Cache Management Patterns

// Advanced cache management composable
export function useCacheManager() {
  const queryCache = useQueryCache()
  
  // Get all cached data
  function getAllCachedData() {
    // Note: This is conceptual - actual implementation may vary
    return queryCache.getQueriesData()
  }
  
  // Clear specific cache patterns
  function clearProductCaches() {
    queryCache.removeQueries({ 
      predicate: (query) => query.queryKey[0] === 'products' 
    })
  }
  
  // Warm up cache with essential data
  async function warmupCache() {
    try {
      // Load essential data into cache
      const products = await fetchAllProducts()
      queryCache.setQueryData(['products'], products)
      
      // Pre-cache first few product details
      const firstFiveProducts = products.slice(0, 5)
      await Promise.all(
        firstFiveProducts.map(async (product) => {
          const details = await fetchProductDetails(product.id)
          queryCache.setQueryData(['product-details', product.id], details)
        })
      )
      
      console.log('βœ… Cache warmed up successfully')
    } catch (error) {
      console.error('❌ Cache warmup failed:', error)
    }
  }
  
  return {
    getAllCachedData,
    clearProductCaches,
    warmupCache
  }
}javascript
// Old way with Pinia - you need to do everything yourself
const useProductStore = defineStore('products', {
  state: () => ({
    products: [],
    loading: false,
    error: null
  }),
  actions: {
    async fetchProducts() {
      this.loading = true
      try {
        const response = await fetch('/api/products')
        this.products = await response.json()
      } catch (error) {
        this.error = error
      } finally {
        this.loading = false
      }
    }
  }
})

// New way with Pinia Colada - everything is automatic!
const { data: products, isPending, error } = useQuery({
  key: ['products'],
  query: () => fetch('/api/products').then(res => res.json())
})

See? With Pinia Colada, we can accomplish the same task with much less code!

Why Should We Use Pinia Colada? Our Store's Needs

In our online store, we face these problems:

  1. Customers view the same product multiple times - Instead of fetching from the server each time, wouldn't it be better to fetch once and store it?

  2. Showing loading text is boring - Writing loading and error states everywhere is tedious.

  3. It should appear immediately when I add to cart - Instead of waiting for server confirmation, if it appears in the cart, the customer would be happy (optimistic update).

  4. Don't lose everything when the page refreshes - With caching, data can be stored for a while.

Pinia Colada solves all these problems!

Let's Get Started: Installing Pinia Colada

Step 1: Install Packages

Let's open our terminal and write these commands:

# If using npm
npm install pinia @pinia/colada

# If using yarn
yarn add pinia @pinia/colada

Step 2: Add to Our Vue Project

Let's open our main.js file and introduce Pinia Colada to our project:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { PiniaColada } from '@pinia/colada'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

// First install Pinia (prepare our warehouse)
app.use(pinia)

// Then add Pinia Colada (set up our smart shipping system)
app.use(PiniaColada, {
  // Optional settings
  queryOptions: {
    staleTime: 30_000, // Data stays fresh for 30 seconds
  }
})

app.mount('#app')

Building Our Online Store: Product List

Now let's build a real e-commerce application. First, let's list our products.

Preparing Our API Functions

Let's create an api/products.js file:

const API_URL = 'https://api.mystore.com'

// Fetch all products
export async function fetchAllProducts() {
  const response = await fetch(`${API_URL}/products`)
  if (!response.ok) throw new Error('Products could not be loaded!')
  return response.json()
}

// Fetch single product details
export async function fetchProductDetails(productId) {
  const response = await fetch(`${API_URL}/products/${productId}`)
  if (!response.ok) throw new Error('Product not found!')
  return response.json()
}

// Fetch products by category
export async function fetchProductsByCategory(category) {
  const response = await fetch(`${API_URL}/products?category=${category}`)
  if (!response.ok) throw new Error('Category products could not be loaded!')
  return response.json()
}

Product List Component

Let's create a ProductList.vue component:

<template>
  <div class="product-list">
    <h1>πŸ›οΈ Welcome to Our Online Store!</h1>
    
    <!-- Loading state -->
    <div v-if="productsLoading && !productList.data" class="loading">
      <div class="spinner">πŸ”„</div>
      <p>Products are being prepared...</p>
    </div>

    <!-- Error state -->
    <div v-else-if="productList.error" class="error">
      <p>😒 Something went wrong: {{ productList.error.message }}</p>
      <button @click="refresh()">πŸ”„ Try Again</button>
    </div>

    <!-- Success state - Products -->
    <div v-else-if="productList.data" class="products">
      <!-- Update indicator -->
      <div v-if="productsLoading" class="updating">
        ✨ Checking for new products...
      </div>
      
      <div class="product-grid">
        <div 
          v-for="product in productList.data" 
          :key="product.id" 
          class="product-card"
          @mouseenter="preloadProduct(product.id)"
        >
          <img :src="product.image" :alt="product.name" />
          <h3>{{ product.name }}</h3>
          <p class="price">${{ product.price }}</p>
          <router-link :to="`/product/${product.id}`" class="details-button">
            πŸ‘οΈ View Details
          </router-link>
        </div>
      </div>
      
      <button @click="refresh()" class="refresh-button">
        πŸ”„ Refresh List
      </button>
    </div>
  </div>
</template>

<script setup>
import { useQuery, useQueryCache } from '@pinia/colada'
import { fetchAllProducts, fetchProductDetails } from '@/api/products'

// Use query cache (for cache management)
const queryCache = useQueryCache()

// Product list query
const {
  state: productList,        // Data state
  asyncStatus: productsLoading,  // Is it loading?
  refresh,                   // Refresh function
} = useQuery({
  key: ['product-list'],     // Unique key
  query: fetchAllProducts,   // Data fetching function
  staleTime: 5 * 60 * 1000,  // Fresh for 5 minutes
})

// Preload product details on mouse hover
function preloadProduct(productId) {
  // Check if data is already in cache
  const cachedData = queryCache.getQueryData(['product-details', productId])
  
  if (!cachedData) {
    // If not in cache, trigger a background query
    // This will start loading the data in the background
    const { state } = useQuery({
      key: ['product-details', productId],
      query: () => fetchProductDetails(productId),
      enabled: true // Force enable even if component doesn't need it yet
    })
  }
}
</script>

<style scoped>
.product-list {
  padding: 20px;
}

.loading {
  text-align: center;
  padding: 50px;
}

.spinner {
  font-size: 48px;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
  margin: 20px 0;
}

.product-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 15px;
  transition: transform 0.2s;
}

.product-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}

.price {
  font-size: 24px;
  color: #27ae60;
  font-weight: bold;
}
</style>

Product Detail Page: Add to Cart Feature

Now let's show details of a single product and add the "add to cart" feature:

<template>
  <div class="product-details">
    <!-- Loading -->
    <div v-if="loading === 'loading'" class="loading">
      <p>πŸ” Fetching product details...</p>
    </div>

    <!-- Error -->
    <div v-else-if="product.error" class="error">
      <p>πŸ˜” Product not found: {{ product.error.message }}</p>
      <router-link to="/products">⬅️ Back to Product List</router-link>
    </div>

    <!-- Product Details -->
    <div v-else-if="product.data" class="product-info">
      <div class="product-image">
        <img :src="product.data.image" :alt="product.data.name" />
      </div>
      
      <div class="product-details-content">
        <h1>{{ product.data.name }}</h1>
        <p class="description">{{ product.data.description }}</p>
        <p class="price">πŸ’° ${{ product.data.price }}</p>
        <p class="stock">πŸ“¦ Stock: {{ product.data.stock }} units</p>
        
        <!-- Add to Cart Section -->
        <div class="add-to-cart-section">
          <label>Quantity:</label>
          <input 
            v-model.number="quantity" 
            type="number" 
            min="1" 
            :max="product.data.stock"
          />
          
          <button 
            @click="addToCart()"
            :disabled="cartMutation.asyncStatus === 'loading' || product.data.stock === 0"
            class="add-to-cart-button"
          >
            <span v-if="cartMutation.asyncStatus === 'loading'">
              ⏳ Adding...
            </span>
            <span v-else>
              πŸ›’ Add to Cart
            </span>
          </button>
        </div>

        <!-- Success Message -->
        <div v-if="added" class="success-message">
          βœ… Product added to cart!
        </div>

        <!-- Error Message -->
        <div v-if="cartMutation.state.error" class="error-message">
          ❌ {{ cartMutation.state.error.message }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { useQuery, useMutation, useQueryCache } from '@pinia/colada'
import { fetchProductDetails } from '@/api/products'
import { addProductToCart } from '@/api/cart'

const route = useRoute()
const queryCache = useQueryCache()
const quantity = ref(1)
const added = ref(false)

// Product details query
const {
  state: product,
  asyncStatus: loading
} = useQuery({
  key: () => ['product-details', route.params.id],
  query: () => fetchProductDetails(route.params.id),
  enabled: () => !!route.params.id // Run if ID exists
})

// Add to cart mutation
const cartMutation = useMutation({
  mutation: ({ productId, quantity }) => addProductToCart(productId, quantity),
  
  // On success
  onSuccess(result, { productId }) {
    // Update cart
    queryCache.invalidateQueries({ key: ['cart'] })
    
    // Update product stock info
    queryCache.invalidateQueries({ key: ['product-details', productId] })
    
    // Show success message
    added.value = true
    setTimeout(() => {
      added.value = false
    }, 3000)
    
    // Reset quantity
    quantity.value = 1
  },
  
  // On error
  onError(error) {
    console.error('Error adding to cart:', error)
  }
})

function addToCart() {
  if (product.value.data) {
    cartMutation.mutate({
      productId: product.value.data.id,
      quantity: quantity.value
    })
  }
}
</script>

Category Filtering: Smart Data Management

Now let's add the feature to filter products by categories:

<template>
  <div class="category-filter">
    <h2>🏷️ Categories</h2>
    
    <!-- Category Buttons -->
    <div class="category-buttons">
      <button 
        v-for="category in categories" 
        :key="category.id"
        @click="selectedCategory = category.id"
        :class="{ active: selectedCategory === category.id }"
        class="category-button"
      >
        {{ category.icon }} {{ category.name }}
      </button>
    </div>

    <!-- Filtered Products -->
    <div v-if="categoryProducts.state.data" class="filtered-products">
      <h3>
        {{ findCategory(selectedCategory).name }} Category 
        ({{ categoryProducts.state.data.length }} products)
      </h3>
      
      <!-- Loading indicator -->
      <div v-if="categoryProducts.asyncStatus === 'loading'" class="filter-loading">
        πŸ”„ Applying filter...
      </div>

      <!-- Product List -->
      <div class="product-grid">
        <div 
          v-for="product in categoryProducts.state.data" 
          :key="product.id"
          class="product-card"
        >
          <img :src="product.image" :alt="product.name" />
          <h4>{{ product.name }}</h4>
          <p class="price">${{ product.price }}</p>
          <button @click="quickAddToCart(product.id)" class="quick-add">
            ⚑ Quick Add
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
import { useQuery, useMutation, useQueryCache } from '@pinia/colada'
import { fetchProductsByCategory } from '@/api/products'
import { addProductToCart } from '@/api/cart'

const queryCache = useQueryCache()

// Categories
const categories = [
  { id: 'electronics', name: 'Electronics', icon: 'πŸ“±' },
  { id: 'clothing', name: 'Clothing', icon: 'πŸ‘•' },
  { id: 'books', name: 'Books', icon: 'πŸ“š' },
  { id: 'toys', name: 'Toys', icon: '🧸' },
  { id: 'sports', name: 'Sports', icon: '⚽' }
]

const selectedCategory = ref('electronics')

// Category-based product query - with reactive key
const categoryProducts = useQuery({
  key: () => ['category-products', selectedCategory.value],
  query: () => fetchProductsByCategory(selectedCategory.value),
  staleTime: 2 * 60 * 1000, // 2 minutes
})

// Quick add to cart
const quickAddMutation = useMutation({
  mutation: (productId) => addProductToCart(productId, 1),
  onSuccess() {
    queryCache.invalidateQueries({ key: ['cart'] })
    alert('βœ… Product added to cart!')
  }
})

function findCategory(categoryId) {
  return categories.find(c => c.id === categoryId)
}

function quickAddToCart(productId) {
  quickAddMutation.mutate(productId)
}

// New data is automatically fetched when category changes
watch(selectedCategory, (newCategory) => {
  console.log('πŸ“‚ Category changed:', newCategory)
  // Pinia Colada automatically fetches new data!
})
</script>

Cart Management: Amazing Experience with Optimistic Updates

Now we've reached the most exciting part! We'll use "optimistic updates" in cart management. What does this mean? When a user adds something to their cart, we'll show it immediately without waiting for server confirmation!

<template>
  <div class="cart">
    <h2>πŸ›’ My Cart</h2>
    
    <!-- Cart Summary -->
    <div class="cart-summary">
      <p>πŸ“¦ Total Items: {{ cartData.data?.items?.length || 0 }}</p>
      <p>πŸ’° Total Amount: ${{ cartData.data?.totalAmount || 0 }}</p>
    </div>

    <!-- Cart Contents -->
    <div v-if="cartData.data?.items" class="cart-items">
      <div 
        v-for="item in cartData.data.items" 
        :key="item.id"
        class="cart-item"
      >
        <img :src="item.product.image" :alt="item.product.name" />
        
        <div class="product-info">
          <h4>{{ item.product.name }}</h4>
          <p>${{ item.product.price }}</p>
        </div>
        
        <!-- Quantity Control -->
        <div class="quantity-control">
          <button @click="updateQuantity(item.id, item.quantity - 1)">βž–</button>
          <span>{{ item.quantity }}</span>
          <button @click="updateQuantity(item.id, item.quantity + 1)">βž•</button>
        </div>
        
        <p class="subtotal">${{ item.product.price * item.quantity }}</p>
        
        <button @click="removeItem(item.id)" class="remove-button">
          πŸ—‘οΈ
        </button>
      </div>
    </div>

    <!-- Updating indicator -->
    <div v-if="hasActiveUpdates" class="updating-banner">
      ⏳ Cart is updating...
    </div>
    
    <!-- Empty Cart -->
    <div v-else class="empty-cart">
      <p>πŸ˜… Your cart is empty!</p>
      <router-link to="/products">πŸ›οΈ Start Shopping</router-link>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useQuery, useMutation, useQueryCache, useMutationState } from '@pinia/colada'
import { fetchCart, updateCartItem, removeCartItem } from '@/api/cart'

const queryCache = useQueryCache()

// Fetch cart data
const cartData = useQuery({
  key: ['cart'],
  query: fetchCart,
  refetchOnWindowFocus: true, // Update when window gets focus
  staleTime: 30_000, // Fresh for 30 seconds
})

// Quantity update mutation
const quantityUpdateMutation = useMutation({
  key: ['cart-update'], // Key for state tracking
  mutation: ({ itemId, newQuantity }) => updateCartItem(itemId, newQuantity),
  
  // Optimistic Update - Show immediately in UI!
  onMutate({ itemId, newQuantity }) {
    // Get current cart data
    const currentCart = queryCache.getQueryData(['cart'])
    
    if (currentCart?.items) {
      // Calculate new data
      const updatedItems = currentCart.items.map(item => 
        item.id === itemId ? { ...item, quantity: newQuantity } : item
      )
      
      // Update total amount
      const newTotal = updatedItems.reduce(
        (total, item) => total + (item.product.price * item.quantity), 
        0
      )
      
      // Update cache immediately (Optimistic Update!)
      queryCache.setQueryData(['cart'], {
        ...currentCart,
        items: updatedItems,
        totalAmount: newTotal
      })
    }
    
    // Save old data (to restore on error)
    return { oldCart: currentCart }
  },
  
  // Always refetch cart in the end
  onSettled() {
    queryCache.invalidateQueries(['cart'])
  },
  
  // Restore old data on error
  onError(error, { itemId, newQuantity }, context) {
    console.error('Update error:', error)
    if (context?.oldCart) {
      queryCache.setQueryData(['cart'], context.oldCart)
    }
  }
})

// Item removal mutation
const itemRemovalMutation = useMutation({
  key: ['cart-remove'],
  mutation: (itemId) => removeCartItem(itemId),
  
  // Optimistic Update - Remove immediately!
  onMutate(itemId) {
    const currentCart = queryCache.getQueryData(['cart'])
    
    if (currentCart?.items) {
      const updatedItems = currentCart.items.filter(
        item => item.id !== itemId
      )
      
      queryCache.setQueryData(['cart'], {
        ...currentCart,
        items: updatedItems,
        totalAmount: updatedItems.reduce(
          (total, item) => total + (item.product.price * item.quantity), 
          0
        )
      })
    }
    
    return { oldCart: currentCart }
  },
  
  onSuccess() {
    queryCache.invalidateQueries(['cart'])
  },
  
  onError(error, itemId, context) {
    if (context?.oldCart) {
      queryCache.setQueryData(['cart'], context.oldCart)
    }
  }
})

// Track active mutations
const updateStatus = useMutationState({ key: ['cart-update'] })
const removeStatus = useMutationState({ key: ['cart-remove'] })

const hasActiveUpdates = computed(() => 
  updateStatus.asyncStatus === 'loading' || 
  removeStatus.asyncStatus === 'loading'
)

// Helper functions
function updateQuantity(itemId, newQuantity) {
  if (newQuantity < 1) return
  quantityUpdateMutation.mutate({ itemId, newQuantity })
}

function removeItem(itemId) {
  if (confirm('Are you sure you want to remove this item from your cart?')) {
    itemRemovalMutation.mutate(itemId)
  }
}
</script>

Advanced Features: Performance Optimizations

1. Prefetching: Loading Data Before Users Need It

One of Pinia Colada's most powerful features is prefetchQuery. This allows you to load data in the background before users actually need it, creating an incredibly smooth user experience!

<template>
  <div class="smart-product-list">
    <h2>🧠 Smart Product List with Prefetching</h2>
    
    <div class="product-grid">
      <div 
        v-for="product in products.data" 
        :key="product.id"
        class="product-card"
        @mouseenter="preloadProductDetails(product.id)"
        @click="navigateToProduct(product.id)"
      >
        <img :src="product.image" :alt="product.name" />
        <h3>{{ product.name }}</h3>
        <p class="price">${{ product.price }}</p>
        
        <!-- Show if data is already prefetched -->
        <div v-if="isPrefetched(product.id)" class="prefetched-indicator">
          ⚑ Ready to view instantly!
        </div>
      </div>
    </div>
    
    <!-- Prefetch Statistics -->
    <div class="prefetch-stats">
      <h4>πŸ“Š Prefetch Statistics</h4>
      <p>Products prefetched: {{ prefetchedCount }}</p>
      <p>Cache hit rate: {{ cacheHitRate }}%</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useQuery, useQueryCache } from '@pinia/colada'
import { useRouter } from 'vue-router'
import { fetchAllProducts, fetchProductDetails } from '@/api/products'

const router = useRouter()
const queryCache = useQueryCache()
const prefetchedIds = ref(new Set())
const prefetchAttempts = ref(0)
const cacheHits = ref(0)

// Main products query
const products = useQuery({
  key: ['products'],
  query:

```javascript
// composables/useProducts.js
import { computed } from 'vue'
import { useQuery, useQueryCache, defineQueryOptions } from '@pinia/colada'
import { fetchAllProducts, fetchProductsByCategory } from '@/api/products'

// Define query options
export const productsQueryOptions = defineQueryOptions(() => ({
  key: ['products'],
  query: fetchAllProducts,
  staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
  gcTime: 30 * 60 * 1000,   // Keep in cache for 30 minutes
}))

// Reusable composable
export function useProducts() {
  const queryCache = useQueryCache()
  
  const {
    state: allProducts,
    asyncStatus: isLoading,
    refresh
  } = useQuery(productsQueryOptions)

  // Computed values
  const productCount = computed(() => allProducts.value.data?.length || 0)
  const hasProducts = computed(() => productCount.value > 0)
  
  // Helper functions
  function findProduct(id) {
    return allProducts.value.data?.find(product => product.id === id)
  }
  
  // Preload a product (useful on hover!)
  function preloadProduct(id) {
    const product = findProduct(id)
    if (product) {
      queryCache.setQueryData(['product-details', id], product)
    }
  }
  
  return {
    allProducts,
    productCount,
    hasProducts,
    isLoading,
    refresh,
    findProduct,
    preloadProduct
  }
}

2. Automatic Refresh Strategies

We want our data to always be current. Pinia Colada makes this very easy:

<template>
  <div class="live-sales-data">
    <h3>πŸ“Š Live Sales Statistics</h3>
    
    <!-- Statistics -->
    <div v-if="salesData.data" class="statistics">
      <div class="stat-card">
        <h4>Today's Sales</h4>
        <p class="big-number">{{ salesData.data.todaySales }}</p>
      </div>
      
      <div class="stat-card">
        <h4>Active Users</h4>
        <p class="big-number">{{ salesData.data.activeUsers }}</p>
      </div>
      
      <div class="stat-card">
        <h4>Items in Cart</h4>
        <p class="big-number">{{ salesData.data.itemsInCart }}</p>
      </div>
    </div>
    
    <!-- Status Indicators -->
    <div class="status-info">
      <p>⏰ Last Update: {{ formatDate(lastUpdate) }}</p>
      <p>πŸ”„ Next Update: {{ remainingTime }} seconds</p>
      
      <label>
        <input v-model="autoRefresh" type="checkbox" />
        Auto Refresh (every 30 seconds)
      </label>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useQuery } from '@pinia/colada'
import { fetchLiveSalesData } from '@/api/dashboard'

const autoRefresh = ref(true)
const lastUpdate = ref(new Date())
const counter = ref(30)

let counterInterval = null

// Live sales data - with auto refresh
const salesData = useQuery({
  key: ['live-sales-data'],
  query: async () => {
    const data = await fetchLiveSalesData()
    lastUpdate.value = new Date()
    counter.value = 30
    return data
  },
  
  // Auto refresh settings
  refetchInterval: computed(() => 
    autoRefresh.value ? 30_000 : false
  ),
  refetchOnWindowFocus: true,  // When window gets focus
  refetchOnReconnect: true,    // When internet connection returns
  
  staleTime: 10_000, // Fresh for 10 seconds
})

const remainingTime = computed(() => counter.value)

function formatDate(date) {
  return new Intl.DateTimeFormat('en-US', {
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  }).format(date)
}

// For counter
onMounted(() => {
  counterInterval = setInterval(() => {
    if (counter.value > 0) counter.value--
  }, 1000)
})

onUnmounted(() => {
  if (counterInterval) clearInterval(counterInterval)
})
</script>

3. Error Management and Retry Strategies

Sometimes things can go wrong. Pinia Colada makes error management easy:

// Global error management - main.js
import { PiniaColadaQueryHooksPlugin } from '@pinia/colada'

app.use(PiniaColadaQueryHooksPlugin, {
  onError(error, query) {
    console.error('Query error:', query.key, error)
    
    // Show notification to user
    if (error.status === 404) {
      showNotification('Content you are looking for was not found 😒', 'error')
    } else if (error.status === 500) {
      showNotification('There is a problem with the server, please try again later πŸ”§', 'error')
    } else if (error.message.includes('Network')) {
      showNotification('Check your internet connection πŸ“‘', 'warning')
    }
  }
})

// Query with custom retry logic
const { data, error, refetch } = useQuery({
  key: ['critical-data'],
  query: fetchCriticalData,
  
  // Retry settings
  retry: 3, // Try 3 times
  retryDelay: (attemptIndex) => {
    // Increase wait time with each attempt
    // 1st attempt: 1 second
    // 2nd attempt: 2 seconds
    // 3rd attempt: 4 seconds
    return Math.min(1000 * Math.pow(2, attemptIndex), 30000)
  },
  
  // Custom error check
  retryOnError: (error) => {
    // Don't retry on 404 errors
    if (error.status === 404) return false
    // Retry on other errors
    return true
  }
})

Advanced Features: Infinite Scroll

In e-commerce sites, there can be too many products. Instead of loading all at once, let's load them as the user scrolls down:

<template>
  <div class="infinite-product-list">
    <h2>πŸ“œ Infinite Product List</h2>
    
    <!-- Products -->
    <div class="product-grid">
      <div 
        v-for="product in allProducts" 
        :key="product.id"
        class="product-card"
      >
        <img :src="product.image" :alt="product.name" />
        <h4>{{ product.name }}</h4>
        <p>${{ product.price }}</p>
      </div>
    </div>
    
    <!-- Loading indicator -->
    <div ref="observerTarget" class="loading-indicator">
      <div v-if="loadingMore">
        πŸ”„ Loading more products...
      </div>
      <div v-else-if="!hasMore">
        βœ… All products loaded!
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useInfiniteQuery } from '@pinia/colada'
import { fetchPaginatedProducts } from '@/api/products'

const observerTarget = ref(null)
let observer = null

// Infinite query
const { 
  state: pages, 
  loadMore, 
  asyncStatus 
} = useInfiniteQuery({
  key: ['infinite-products'],
  
  // Query function for each page
  query: async ({ pageParam }) => {
    if (pageParam === null) return null
    
    const result = await fetchPaginatedProducts({
      page: pageParam,
      limit: 20
    })
    
    return {
      products: result.data,
      nextPage: result.nextPage
    }
  },
  
  // Initial page parameter
  initialPageParam: 1,
  
  // Determine next page parameter
  getNextPageParam: (lastPage) => {
    return lastPage?.nextPage || null
  }
})

// Computed values
const allProducts = computed(() => {
  if (!pages.value.data) return []
  return pages.value.data.pages
    .filter(page => page !== null)
    .flatMap(page => page.products)
})

const loadingMore = computed(() => 
  asyncStatus.value === 'loading'
)

const hasMore = computed(() => {
  if (!pages.value.data) return true
  const lastPage = pages.value.data.pages[pages.value.data.pages.length - 1]
  return lastPage?.nextPage !== null
})

// Intersection Observer setup
onMounted(() => {
  observer = new IntersectionObserver(
    (entries) => {
      const target = entries[0]
      if (target.isIntersecting && hasMore.value && !loadingMore.value) {
        loadMore()
      }
    },
    { threshold: 0.5 }
  )
  
  if (observerTarget.value) {
    observer.observe(observerTarget.value)
  }
})

onUnmounted(() => {
  if (observer) {
    observer.disconnect()
  }
})
</script>

Testing: Code Security

Writing tests is very important to ensure our code works properly. Testing with Pinia Colada is also very easy:

// tests/ProductList.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { mount } from '@vue/test-utils'
import { useQuery } from '@pinia/colada'
import ProductList from '@/components/ProductList.vue'

describe('ProductList Component', () => {
  beforeEach(() => {
    // Create a new Pinia instance before each test
    setActivePinia(createPinia())
  })

  it('successfully loads and displays products', async () => {
    // Mock data
    const mockProducts = [
      { id: 1, name: 'Laptop', price: 1500 },
      { id: 2, name: 'Mouse', price: 20 }
    ]

    // Mock API call
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProducts)
    })
    global.fetch = mockFetch

    // Mount component
    const wrapper = mount(ProductList)

    // Wait a bit (for query to complete)
    await new Promise(resolve => setTimeout(resolve, 100))

    // Check that products are displayed
    expect(wrapper.text()).toContain('Laptop')
    expect(wrapper.text()).toContain('$1500')
    expect(wrapper.text()).toContain('Mouse')
    expect(wrapper.text()).toContain('$20')
  })

  it('handles error state properly', async () => {
    // Faulty API call
    const mockFetch = vi.fn().mockRejectedValue(
      new Error('Server error')
    )
    global.fetch = mockFetch

    const wrapper = mount(ProductList)
    
    await new Promise(resolve => setTimeout(resolve, 100))

    // Check that error message is displayed
    expect(wrapper.text()).toContain('Something went wrong')
    expect(wrapper.find('.refresh-button').exists()).toBe(true)
  })
})

Best Practices and Tips

1. Keep Query Keys Organized

Keep your query keys hierarchical and organized:

// βœ… Good example
const QUERY_KEYS = {
  products: {
    all: ['products'],
    detail: (id) => ['products', 'detail', id],
    category: (category) => ['products', 'category', category],
    search: (term) => ['products', 'search', term]
  },
  cart: ['cart'],
  user: {
    profile: ['user', 'profile'],
    orders: ['user', 'orders']
  }
}

// Usage
useQuery({
  key: QUERY_KEYS.products.detail(productId),
  query: () => fetchProductDetails(productId)
})

2. Use Optimistic Updates Wisely

Don't use optimistic updates everywhere. Only use them where they would improve user experience:

// βœ… Good usage - Adding to cart (quick feedback is important)
useMutation({
  mutation: addToCart,
  onMutate: () => {
    // Show immediately in UI
  }
})

// ❌ Bad usage - Payment processing (security is important)
useMutation({
  mutation: processPayment,
  // DON'T use optimistic update!
  // Wait for server confirmation
})

3. Set Cache Times Correctly

Set cache times according to how often the data changes:

// Frequently changing data (stock status)
useQuery({
  key: ['stock', productId],
  query: () => fetchStockStatus(productId),
  staleTime: 30_000, // 30 seconds
  gcTime: 5 * 60_000 // 5 minutes
})

// Rarely changing data (category list)
useQuery({
  key: ['categories'],
  query: fetchCategories,
  staleTime: 24 * 60 * 60_000, // 24 hours
  gcTime: 7 * 24 * 60 * 60_000 // 1 week
})

Pinia Colada vs Others: Comparison

Pinia Colada vs TanStack Query (React Query)

Similarities:

  • Similar API design
  • Cache management
  • Optimistic updates

Pinia Colada Advantages:

  • Optimized for Vue
  • Smaller size (~2kb vs ~25kb)
  • Natural integration with Vue reactivity system
  • Simpler setup

Pinia Colada vs Apollo Client

Apollo Client:

  • GraphQL focused
  • More complex
  • Larger size

Pinia Colada:

  • Perfect for REST APIs
  • Simple and understandable
  • Supports GraphQL but not mandatory

Conclusion: Why Pinia Colada?

Pinia Colada is an amazing tool that makes data management in modern Vue.js applications easier. Here are its main advantages:

  1. πŸš€ Performance: Automatic cache and request deduplication
  2. 😊 Easy to Use: Simple API, less code
  3. 🎯 Vue-Specific: Optimized for Vue 3 and Composition API
  4. πŸ“¦ Small Size: Only ~2kb
  5. πŸ”§ Powerful Features: Optimistic updates, SSR support
  6. πŸ§ͺ Testable: Easy to write tests
  7. πŸ“š Good Documentation: Easy to learn

Who Should Use It?

  • βœ… Anyone using Vue 3
  • βœ… Those building modern, reactive applications
  • βœ… Data-intensive applications like e-commerce, dashboards
  • βœ… Performance-conscious developers
  • βœ… Clean code enthusiasts

Your Learning Journey

  1. Beginner: Start with simple examples in this article
  2. Practice: Try it in your own projects
  3. Deepen: Read the official documentation
  4. Master: Explore advanced features

With Pinia Colada, data management in your Vue.js applications is now much easier and more fun! Go ahead, build your own online store and discover the power of this amazing tool! πŸš€

πŸ’‘ Tip: All code examples in this article are sequential. You can build a complete e-commerce application by following from start to finish!

Happy coding! πŸŽ‰

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment