Pinia Colada Documentation

repository·main·Indexed 24 days ago

https://github.com/posva/pinia-colada

A smart data fetching layer for Vue.js applications built on top of Pinia. It simplifies async state management with features including automatic caching, request deduplication, and optimistic updates. It provides `useQuery` for reading data, `useMutation` for write operations, and a centralized `useQueryCache` for managing query invalidation and batch operations. Includes a Nuxt module (@pinia/colada-nuxt) for SSR and a TanStack Query Vue v5 compatibility layer (@pinia/colada-plugin-tanstack-compat).

Tokens
57.9K
Snippets
153
Records
240
Agent score
76%

What's inside Pinia Colada

  1. Core features of Pinia Colada

    main

    Pinia Colada provides several built-in capabilities for managing asynchronous state:

    • Caching & Deduplication: Automatically manages cached data and prevents redundant simultaneous requests for the same key.
    • Invalidation: Provides mechanisms to invalidate cached data.
    • Flexibility: Works with any function that returns a Promise, including fetch, axios, GraphQL, or WebSockets.
    • TypeScript Support: Full type safety for queries and returned data.
    • DevTools Integration: Advanced debugging tools to inspect loading/error states and monitor fetch frequency.
    • Performance: Optimized for efficient reactivity and minimal overhead in Vue applications.
  2. Access Mutation Extensions via Mutation Cache

    main

    Unlike useQuery, mutation extensions are NOT directly available on the useMutation() return value because Pinia Colada does not spread entry.ext properties into the return object.

    To access these properties (like isPending or isIdle), you must use the useMutationCache hook and retrieve the entry by its key.

    import { useMutation, useMutationCache } from '@pinia/colada'
    
    const { mutate } = useMutation({
      key: ['myMutation'],
      mutation: async (data) => {
        /* ... */
      },
    })
    
    const mutationCache = useMutationCache()
    
    // After calling mutate(), access extensions via cache
    const entry = mutationCache.getEntries({ key: ['myMutation'] })[0]
    const isPending = entry?.ext.isPending.value
  3. Understand the difference between invalidating and canceling queries

    main

    In Pinia Colada, there is a distinction between invalidating a query and canceling it:

    • Invalidating: Marks the existing data as stale. The next time the query is requested, it will trigger a new fetch to refresh the data.
    • Canceling: Stops an ongoing asynchronous request. This is useful for preventing unnecessary network traffic or processing results from a request that is no longer relevant (e.g., a user navigated away or changed search parameters).
  4. Leverage hierarchical query keys for invalidation

    main

    Query keys are hierarchical. You can nest keys to create relationships between queries. This allows you to invalidate a broad group of queries by matching a prefix (root key) or be more specific by including more segments.

    Matching Rules:

    • ['doc', 2] and ['doc', '2'] are different.
    • In objects, undefined is stripped out (e.g., ['doc', { a: undefined }] matches ['doc', {}]), but null is preserved.
    • Arrays are partially matched: ['doc', ['a', 'b']] matches ['doc', ['a']] but not ['doc', ['a', 'b', 'c']].
    // gets the product with all its details
    useQuery({
      key: () => ['products', productId.value],
      query: () => getProductById(productId.value),
    })
    
    // gets a product summary suited for searches
    useQuery({
      key: () => ['products', productId.value, { searchResult: true }],
      query: () => getProductSummaryById(productId.value),
    })
    
    // Invalidating the root key ['products', productId.value] 
    // will invalidate both the full product and the search summary.
    queryCache.invalidateQueries({ key: ['products', productId.value] })
  5. Sync data after mutations

    main

    Mutations are typically used to change data. To keep your UI in sync with the server after a mutation, use one of these two strategies:

    1. Invalidate queries: Trigger a refetch of the queries that depend on the data that was just changed.
    2. Optimistic updates: Update the local cache/UI immediately before the mutation completes to provide a faster user experience.
  6. Configure global mutation hooks

    main

    You can define global hooks that run for every mutation in your application. These include:

    • onMutate
    • onSuccess
    • onError
    • onSettled

    These are configured via mutationOptions when installing PiniaColada. If you need to extend the actual return values of useMutation() (e.g., adding custom counters or logging), you should use the Plugin system instead.

  7. Understand the difference between `state.status` and `asyncStatus`

    main

    Pinia Colada separates the status of the data from the status of the network request to provide better UI control:

    • state.status (or status): Represents the status of the data.

      • 'pending': The query hasn't resolved yet (initial state).
      • 'success': The query successfully returned data.
      • 'error': The query failed.
    • asyncStatus: Represents the status of the query call (the network/async activity).

      • 'idle': The query is not currently fetching.
      • 'loading': The query is currently in the process of fetching data.

    Separating these allows you to distinguish between a component that is waiting for its first load (pending + loading) and a component that is performing a background refresh (success + loading).

  8. Use signal properties to discard query results

    main

    Pinia Colada provides signal properties that allow you to discard results in two specific scenarios:

    1. New queries of the same key: When a new query is initiated with the same key as a currently running query, the previous one can be discarded.
    2. Manual cancellation: When you explicitly call cancelQueries, the results of the targeted queries are discarded.
  9. How SSR works with Pinia Colada

    main

    Unlike Nuxt's native useFetch, Pinia Colada's useQuery does not require an explicit await for Server-Side Rendering (SSR) to work.

    Automatic SSR

    useQuery uses onServerPrefetch internally. This means:

    1. On the server, useQuery registers via onServerPrefetch.
    2. The query runs and awaits during the server-side rendering process.
    3. Data is serialized to the payload and hydrated on the client.

    When to use await

    You should still use await with useQuery if you want to block client-side navigation until the data is loaded. Without await, the page renders immediately and shows loading states while the data populates.

    const { data, refresh } = useQuery({
      key: ['products'],
      query: () => $fetch('/api/products'),
    })
    // Block navigation until products load
    await refresh()
    // Pinia Colada: no await needed, SSR works automatically
    const { data } = useQuery({
      key: ['products'],
      query: () => $fetch('/api/products'),
    })
  10. Add entry extensions to query or mutation return values

    main

    Entry extensions are properties stored on entry.ext that become available on the objects returned by useQuery() and useMutation().

    Implementation Rules:

    1. Use the extend action: You should only add new keys to entry.ext during the extend action. This action is called only once per entry and is shared across all plugins.
    2. Do not replace the object: Do not assign a completely new object to entry.ext; instead, add properties to it.
    3. Define keys early: You cannot add new keys to entry.ext later (e.g., in fetch or setEntryState). All keys must be defined during extend so they are available on the return value from the start.
    4. Reactive extensions: When defining reactive extensions (like shallowRef), ensure they are created inside scope.run() so they are properly disposed of when the entry is removed.

    TypeScript Augmentation:

    Augment UseQueryEntryExtensions or UseMutationEntryExtensions to make these fields visible to consumers. You must preserve all generic parameters.

    // Example: Adding a reactive field to a Query
    import { type PiniaColadaPlugin } from '@pinia/colada'
    
    export function PiniaColadaFeaturePlugin(): PiniaColadaPlugin {
      return ({ queryCache, scope }) => {
        queryCache.$onAction(({ name, args }) => {
          if (name === 'extend') {
            const [entry] = args
            scope.run(() => {
              entry.ext.myField = shallowRef<string>('initial value')
            })
          }
        })
      }
    }
    
    declare module '@pinia/colada' {
      interface UseQueryEntryExtensions<TData, TError, TDataInitial> {
        myField: ShallowRef<string>
      }
    }
  11. Workflow for using Pinia Colada plugins

    main

    Most plugins follow a standard three-step workflow:

    1. Install the package: If the plugin is not built into the core @pinia/colada package, install it as a separate dependency.
    2. Register the plugin: Add the plugin to the plugins array in the PiniaColada configuration during application setup.
    3. Configure the plugin: You can optionally configure plugins globally via plugin options or locally via per-query/mutation options.
  12. Understand query meta access patterns

    main

    Metadata is stored in two distinct locations within a query entry:

    1. entry.meta: The resolved value. This is the result of the meta function/ref, computed once when the entry is created.
    2. entry.options.meta: The original value. This contains the raw function, ref, or object passed into the useQuery options.

    Use entry.meta for logic that needs the actual data, and entry.options.meta if you need to inspect the original configuration.

    import { useQueryCache, useQuery } from '@pinia/colada'
    
    useQuery({
      key: ['time'],
      query: () => fetch('/api/time').then((r) => r.json()),
      meta: () => ({ timestamp: Date.now() }),
    })
    
    // ---cut-start---
    const queryCache = useQueryCache()
    // ---cut-end---
    
    // In your plugin
    queryCache.$onAction(({ name, after, args }) => {
      if (name === 'extend') {
        const [entry] = args
        console.log(entry.meta) // { timestamp: 1234567890 }
        console.log(entry.options?.meta) // [Function: metaFn]
      }
    })