zenstack-pinia-colada
v0.2.1
Published
Pinia Colada Client for consuming ZenStack v3's CRUD service
Maintainers
Readme
ZenStack Pinia Colada
Pinia Colada client for ZenStack - The Smart Data Fetching Layer for Vue 3.
Features
- 🔐 Type-safe - Full TypeScript support with automatic type inference
- ⚡️ Automatic caching - Smart caching powered by Pinia Colada
- 🔄 Optimistic updates - Update UI before server responds
- 🎯 Automatic invalidation - Cache invalidation based on data relationships
- 📦 Zero config - Works out of the box with your ZenStack schema
- 🌳 Tree-shakeable - Only bundle what you use
Installation
npm install zenstack-pinia-colada @pinia/colada pinia
# or
pnpm add zenstack-pinia-colada @pinia/colada pinia
# or
yarn add zenstack-pinia-colada @pinia/colada piniaPrerequisites
- You need a ZenStack project set up (v3.0.0 or higher). See ZenStack documentation for details.
- Generate your ZenStack schema using
npx zenstack generate
Note: This library requires ZenStack v3 to be installed in your project. The library will use your installed version of ZenStack packages.
Quick Start
1. Setup Pinia Colada
// main.ts
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()
app.use(pinia)
app.use(PiniaColada)
app.mount('#app')2. Provide Query Settings (Optional)
<!-- App.vue or layout component -->
<script setup lang="ts">
import { provideQuerySettingsContext } from 'zenstack-pinia-colada'
provideQuerySettingsContext({
endpoint: '/api/model', // default endpoint
logging: true, // enable logging for debugging
})
</script>3. Use in Components
<script setup lang="ts">
import { useClientQueries } from 'zenstack-pinia-colada'
import { schema } from './zenstack/schema-lite'
const queries = useClientQueries(schema)
// Query data
const { data: users, status, error } = queries.user.useFindMany()
// Mutations
const createUser = queries.user.useCreate()
const handleCreateUser = () => {
createUser.mutate({
data: {
email: '[email protected]',
name: 'John Doe',
},
})
}
</script>
<template>
<div>
<div v-if="status === 'pending'">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }} ({{ user.email }})</li>
</ul>
<button @click="handleCreateUser" :disabled="createUser.status === 'pending'">
Create User
</button>
</div>
</template>API Reference
Query Hooks
Each model in your schema gets the following query hooks:
useFindUnique(args, options)- Find a unique recorduseFindFirst(args, options)- Find the first matching recorduseFindMany(args, options)- Find multiple recordsuseInfiniteFindMany(args, options)- Paginated query with infinite loadinguseCount(args, options)- Count recordsuseAggregate(args, options)- Aggregate datauseGroupBy(args, options)- Group records
Query Return Values:
{
data: Ref<T | undefined>, // Query data
error: Ref<Error | null>, // Error if query failed
status: Ref<'pending' | 'success' | 'error'>, // Query status
refresh: () => Promise<void>, // Manually refetch
// ... and more from Pinia Colada
}Mutation Hooks
Each model gets these mutation hooks:
useCreate(options)- Create a recorduseCreateMany(options)- Create multiple recordsuseCreateManyAndReturn(options)- Create and return multiple recordsuseUpdate(options)- Update a recorduseUpdateMany(options)- Update multiple recordsuseUpdateManyAndReturn(options)- Update and return multiple recordsuseUpsert(options)- Create or update a recorduseDelete(options)- Delete a recorduseDeleteMany(options)- Delete multiple records
Mutation Return Values:
{
mutate: (variables: T) => void, // Trigger mutation
mutateAsync: (variables: T) => Promise<R>, // Async mutation
status: Ref<'pending' | 'success' | 'error' | 'idle'>,
data: Ref<R | undefined>, // Mutation result
error: Ref<Error | null>, // Error if mutation failed
// ... and more from Pinia Colada
}Advanced Features
Working with Reactive Parameters
Pinia Colada automatically tracks reactive dependencies in your queries. When using reactive values (refs, computed), wrap your query arguments in a getter function:
import { ref, computed } from 'vue'
const userId = ref('123')
const includeDeleted = ref(false)
// ✅ Correct: Use a getter function
const { data: posts } = queries.post.useFindMany(() => ({
where: {
authorId: userId.value, // Unwrap refs inside the getter
deletedAt: includeDeleted.value ? undefined : null
},
}))
// When userId or includeDeleted changes, the query automatically re-runs!Why use a getter function?
The getter function () => ({...}) allows Pinia Colada to track when your reactive values change and automatically re-fetch the query. Inside the getter, unwrap refs with .value.
Alternative patterns:
// Using computed (also works)
const queryArgs = computed(() => ({
where: { authorId: userId.value }
}))
const { data } = queries.post.useFindMany(queryArgs)
// Static queries (no reactivity needed)
const { data } = queries.post.useFindMany({
where: { published: true } // No getter needed for static values
})Optimistic Updates
Optimistic updates allow the UI to update immediately before the server responds:
const updatePost = queries.post.useUpdate({
optimisticUpdate: true, // Enable optimistic updates
})
updatePost.mutate({
where: { id: '1' },
data: { title: 'New Title' },
})
// UI updates immediately, then syncs with server responseCustom Query Options
Pinia Colada provides many options to customize query behavior:
const { data } = queries.post.useFindMany(
{ where: { published: true } },
{
staleTime: 5000, // Consider data fresh for 5 seconds
gcTime: 300000, // Garbage collection time (default: 5 minutes)
refetchOnMount: true,
refetchOnWindowFocus: true,
enabled: computed(() => isReady.value), // Conditionally enable
}
)Infinite Queries (Pagination)
For paginated data with infinite scrolling:
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = queries.post.useInfiniteFindMany(
{ take: 10, where: { published: true } },
{
getNextPageParam: (lastPage, pages) => {
// Return the cursor for the next page
return lastPage.length === 10 ? pages.length * 10 : undefined
},
}
)Disable Auto Invalidation
By default, mutations automatically invalidate related queries. You can disable this:
const createPost = queries.post.useCreate({
invalidateQueries: false, // Don't auto-invalidate related queries
})Type Safety
All hooks are fully typed based on your ZenStack schema:
// TypeScript knows the exact shape of User
const { data: user } = queries.user.useFindUnique({
where: { id: '1' },
select: { id: true, name: true, email: true },
})
// user is typed as: { id: string; name: string; email: string } | nullComparison with TanStack Query
If you're familiar with @zenstackhq/tanstack-query, the Pinia Colada client offers:
- 🎯 Vue-first design - Built specifically for Vue 3 composition API
- 📦 Smaller bundle - Tree-shakeable ESM-only package
- 🔧 Simpler API - Less configuration, sensible defaults
- 🏪 Pinia integration - Works seamlessly with your Pinia store
- ⚡️ Better performance - Optimized for Vue's reactivity system
Key API Differences:
- Returns Vue
Refobjects instead of plain values - Uses
statusinstead of separateisLoading,isSuccessflags refresh()instead ofrefetch()for manual updates- Direct integration with Pinia's state management
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT
