WIP: This library is under active development and not yet published to npm. The API may change significantly.
Not Production Ready: This is an experimental project. Use at your own risk in production environments.
GraphQL integration for Pinia Colada with Relay-style fragment colocation and normalized cache.
- Type-safe GraphQL queries and mutations
- Relay-style fragment colocation in Vue SFCs
- Data masking for component encapsulation
- Normalized cache with automatic updates
- Optimistic mutations
- Client interceptors for logging/monitoring
# npm
npm install @pinia-colada-graphql/core @pinia/colada pinia graphql
# pnpm
pnpm add @pinia-colada-graphql/core @pinia/colada pinia graphql
# bun
bun add @pinia-colada-graphql/core @pinia/colada pinia graphqlFor Vite projects with fragment colocation support:
npm install @pinia-colada-graphql/unplugin -D// main.ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import { PiniaColada } from "@pinia/colada";
import { PiniaColadaGraphQL } from "@pinia-colada-graphql/core";
import App from "./App.vue";
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(PiniaColada);
app.use(PiniaColadaGraphQL, {
client: {
url: "/graphql",
},
});
app.mount("#app");<script setup lang="ts">
import { useGraphQLQuery } from "@pinia-colada-graphql/core";
import { gql } from "@pinia-colada-graphql/tagged-template";
const GetUsersDocument = gql`
query GetUsers {
users {
id
name
email
}
}
`;
const { data, isLoading, error, refetch } = useGraphQLQuery({
document: GetUsersDocument,
});
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="user in data?.users" :key="user.id">
{{ user.name }}
</li>
</ul>
</template><script setup lang="ts">
import { useGraphQLMutation } from "@pinia-colada-graphql/core";
import { gql } from "@pinia-colada-graphql/tagged-template";
const CreateUserDocument = gql`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
`;
const { mutate, isLoading } = useGraphQLMutation({
document: CreateUserDocument,
invalidateQueries: [["graphql", "GetUsers"]],
});
async function handleSubmit(name: string, email: string) {
await mutate({ name, email });
}
</script>Fragment colocation allows you to declare a component's data dependencies directly within the component file, similar to Relay.
Configure the Vite plugin:
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import graphql, { vueGraphQLBlock } from "@pinia-colada-graphql/unplugin/vite";
export default defineConfig({
plugins: [
vueGraphQLBlock(), // Must be before vue()
vue(),
graphql(),
],
});Define fragments in a <script lang="graphql"> block within your Vue SFC:
<!-- UserCard.vue -->
<script lang="graphql">
fragment UserCard_user on User {
name
email
}
</script>
<script setup lang="ts">
import { useFragment } from "@pinia-colada-graphql/core";
import { UserCard_user, type UserCard_user$key } from "./UserCard.vue?graphql";
const props = defineProps<{
userRef: UserCard_user$key;
}>();
const user = useFragment(UserCard_user, () => props.userRef);
</script>
<template>
<div class="user-card">
<div>{{ user?.name }}</div>
<div>{{ user?.email }}</div>
</div>
</template>Use the fragment in a parent component:
<!-- UserList.vue -->
<script setup lang="ts">
import { useGraphQLQuery } from "@pinia-colada-graphql/core";
import { gql } from "@pinia-colada-graphql/tagged-template";
import UserCard from "./UserCard.vue";
import { UserCard_user } from "./UserCard.vue?graphql";
const GetUsersDocument = gql`
query GetUsers {
users {
id
__typename
...UserCard_user
}
}
${UserCard_user}
`;
const { data } = useGraphQLQuery({
document: GetUsersDocument,
});
</script>
<template>
<UserCard
v-for="user in data?.users"
:key="user.id"
:user-ref="user"
/>
</template>With fragment colocation, parent components only receive the id and __typename fields needed to reference the entity. Child-specific fields like name and email are encapsulated within the child component through useFragment.
Parent Component Child Component
┌─────────────────┐ ┌─────────────────┐
│ useGraphQLQuery │ │ useFragment │
│ │ │ │
│ users: [ │ │ user: │
│ { id, │ ──── │ { name, │
│ __typename }│ ref │ email } │
│ ] │ │ │
└─────────────────┘ └─────────────────┘
│ │
└───────────┬───────────┘
▼
Normalized Cache
(User:1, User:2...)
Execute a GraphQL query with automatic caching.
const { data, isLoading, error, refetch } = useGraphQLQuery({
document: MyQueryDocument,
variables: { id: "1" },
enabled: true,
staleTime: 5000,
gcTime: 300000,
});Options:
| Option | Type | Description |
|---|---|---|
document |
TypedDocumentNode |
The GraphQL query document |
variables |
MaybeRefOrGetter<Variables> |
Query variables (reactive) |
key |
MaybeRefOrGetter<EntryKey> |
Custom cache key |
enabled |
MaybeRefOrGetter<boolean> |
Whether the query is enabled |
staleTime |
number |
Time in ms before data is considered stale |
gcTime |
number |
Time in ms before inactive data is garbage collected |
transform |
(data) => T |
Transform function for the response |
useNormalizedCache |
boolean |
Whether to use normalized caching (default: true) |
Execute a GraphQL mutation with optimistic updates support.
const { mutate, isLoading, error } = useGraphQLMutation({
document: CreateUserDocument,
optimisticResponse: (vars) => ({
createUser: {
id: "temp-id",
__typename: "User",
name: vars.name,
email: vars.email,
},
}),
invalidateQueries: [["graphql", "GetUsers"]],
onSuccess: (data) => {
console.log("User created:", data);
},
});Options:
| Option | Type | Description |
|---|---|---|
document |
TypedDocumentNode |
The GraphQL mutation document |
optimisticResponse |
Response | (vars) => Response |
Optimistic response for immediate UI updates |
invalidateQueries |
EntryKey[] |
Query keys to invalidate after success |
refetchQueries |
EntryKey[] |
Query keys to refetch after success |
onMutate |
(vars) => Context |
Called before mutation executes |
onSuccess |
(data, vars, context) => void |
Called on successful mutation |
onError |
(error, vars, context) => void |
Called on mutation error |
onSettled |
(data, error, vars, context) => void |
Called when mutation settles |
Read fragment data from the normalized cache with data masking.
const user = useFragment(UserCard_user, () => props.userRef);Parameters:
| Parameter | Type | Description |
|---|---|---|
fragmentDoc |
TypedFragmentNode |
The fragment document |
fragmentRef |
MaybeRefOrGetter<FragmentRef> |
Reference to the entity |
options.throwOnMissing |
boolean |
Throw if entity not found (default: false) |
Create a GraphQL client with interceptor support.
const client = createGraphQLClient({
url: "/graphql",
headers: {
Authorization: `Bearer ${token}`,
},
interceptors: {
onRequest: (ctx) => {
console.log("Request:", ctx.operationName);
},
onResponse: (ctx) => {
console.log("Response:", ctx.response);
return ctx.response;
},
onError: (ctx) => {
console.error("Error:", ctx.error);
return ctx.error;
},
},
});Options:
| Option | Type | Description |
|---|---|---|
url |
string |
GraphQL endpoint URL |
fetch |
typeof fetch |
Custom fetch implementation |
headers |
Headers | (ctx) => Headers |
Request headers (static or dynamic) |
credentials |
RequestCredentials |
Fetch credentials option |
interceptors.onRequest |
(ctx) => void |
Called before each request |
interceptors.onResponse |
(ctx) => Response |
Called after each response |
interceptors.onError |
(ctx) => Error | null |
Called on errors (return null to suppress) |
maxRetries |
number |
Maximum retry attempts (default: 0) |
Access the normalized cache store directly.
const cache = useNormalizedCache();
// Read an entity
const user = cache.readFragment<User>("User:1", UserFragment);
// Write to cache
cache.writeFragment("User:1", { name: "Updated Name" });
// Evict an entity
cache.evict("User:1");
// Clear entire cache
cache.clear();Customize how entities are identified and cached:
app.use(PiniaColadaGraphQL, {
client: { url: "/graphql" },
typePolicies: {
User: {
keyFields: ["id"],
},
Product: {
keyFields: ["sku", "warehouse"],
},
Query: {
fields: {
user: {
read: (existing, { args }) => {
// Custom read logic
},
},
},
},
},
});// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import graphql, { vueGraphQLBlock } from "@pinia-colada-graphql/unplugin/vite";
export default defineConfig({
plugins: [
vueGraphQLBlock(), // Must be before vue() - handles <script lang="graphql">
vue(),
graphql({
// Generate TypeScript types for fragments
typescript: true,
}),
],
});The plugin:
- Extracts
<script lang="graphql">blocks from Vue SFCs - Generates TypeScript types for fragments
- Provides virtual module imports (
?graphqlsuffix)
| Package | Description |
|---|---|
@pinia-colada-graphql/core |
Core library with composables and cache |
@pinia-colada-graphql/unplugin |
Vite/Webpack plugin for fragment colocation |
@pinia-colada-graphql/tagged-template |
Tagged template literal for GraphQL |
# Install dependencies
bun install
# Build all packages
bun run build
# Run example
bun run dev
# Type check
bun run typecheck
# Lint
bun run lintMIT