Type-safe TanStack React Query integration for QUESTPIE.
npm install @questpie/tanstack-queryType-safe TanStack React Query integration for QUESTPIE.
- Query Options Factory - Pre-built query/mutation options using queryOptions() and mutationOptions() from TanStack Query
- SSR Ready - Prefetch data on server, hydrate on client
- Type-Safe - Full TypeScript inference from your CMS schema
``bash`
bun add @questpie/tanstack-query questpie @tanstack/react-query
`typescript
// src/lib/queries.ts
import { createClient } from "questpie/client";
import { createQuestpieQueryOptions } from "@questpie/tanstack-query";
import type { AppCMS } from "@/cms";
const cmsClient = createClient
baseURL: "http://localhost:3000",
basePath: "/api/cms",
});
export const q = createQuestpieQueryOptions(cmsClient, {
keyPrefix: ["cms"], // Optional: prefix for all query keys
});
`
`typescript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { q } from "@/lib/queries"
function PostsList() {
// Queries
const { data: posts } = useQuery(q.collections.posts.find({ limit: 10 }))
// Mutations
const queryClient = useQueryClient()
const createPost = useMutation({
...q.collections.posts.create(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cms", "collections", "posts"] })
},
})
return (
Query Options API
$3
`typescript
// Find many
q.collections.posts.find({
where: { published: { eq: true } },
orderBy: { createdAt: "desc" },
limit: 10,
offset: 0,
with: { author: true },
});// Find one
q.collections.posts.findOne({
where: { id: "post-id" },
with: { author: true, comments: true },
});
// Create
q.collections.posts.create();
// Usage: mutation.mutate({ title: "...", content: "..." })
// Update
q.collections.posts.update();
// Usage: mutation.mutate({ id: "...", data: { title: "..." } })
// Delete
q.collections.posts.delete();
// Usage: mutation.mutate({ id: "..." })
// Restore (soft delete)
q.collections.posts.restore();
// Usage: mutation.mutate({ id: "..." })
`$3
`typescript
// Get
q.globals.siteSettings.get();// Update
q.globals.siteSettings.update();
// Usage: mutation.mutate({ data: { siteName: "..." } })
`$3
`typescript
const searchQuery = q.custom.query({
key: ["search", query],
queryFn: () => fetch(/api/search?q=${query}).then((r) => r.json()),
});useQuery(searchQuery);
`SSR Prefetching
$3
`typescript
// src/routes/posts.tsx
import { createFileRoute } from "@tanstack/react-router"
import { useSuspenseQuery } from "@tanstack/react-query"
import { q } from "@/lib/queries"export const Route = createFileRoute("/posts")({
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(
q.collections.posts.find({ limit: 10 })
)
},
component: PostsPage,
})
function PostsPage() {
const { data: posts } = useSuspenseQuery(
q.collections.posts.find({ limit: 10 })
)
return (
{posts.docs.map((post) => (
- {post.title}
))}
)
}
`$3
`typescript
// app/posts/page.tsx
import { HydrationBoundary, dehydrate } from "@tanstack/react-query"
import { getQueryClient } from "@/lib/query-client"
import { q } from "@/lib/queries"
import { PostsList } from "./posts-list"export default async function PostsPage() {
const queryClient = getQueryClient()
await queryClient.prefetchQuery(
q.collections.posts.find({ limit: 10 })
)
return (
)
}
`Optimistic Updates
`typescript
const queryClient = useQueryClient();const updatePost = useMutation({
...q.collections.posts.update(),
onMutate: async ({ id, data }) => {
await queryClient.cancelQueries({
queryKey: ["cms", "collections", "posts"],
});
const previousPosts = queryClient.getQueryData([
"cms",
"collections",
"posts",
]);
queryClient.setQueryData(["cms", "collections", "posts"], (old) => ({
...old,
docs: old.docs.map((post) =>
post.id === id ? { ...post, ...data } : post,
),
}));
return { previousPosts };
},
onError: (err, variables, context) => {
queryClient.setQueryData(
["cms", "collections", "posts"],
context?.previousPosts,
);
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ["cms", "collections", "posts"],
});
},
});
`Infinite Queries
`typescript
import { useInfiniteQuery } from "@tanstack/react-query"
import { cmsClient } from "@/lib/cms-client"function InfinitePosts() {
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["cms", "collections", "posts", "infinite"],
queryFn: ({ pageParam = 0 }) =>
cmsClient.collections.posts.find({
limit: 10,
offset: pageParam,
}),
getNextPageParam: (lastPage, allPages) => {
const totalFetched = allPages.flatMap((p) => p.docs).length
return lastPage.docs.length === 10 ? totalFetched : undefined
},
initialPageParam: 0,
})
return (
{data?.pages.flatMap((page) =>
page.docs.map((post) => {post.title})
)}
{hasNextPage && (
)}
)
}
`Query Key Structure
All query keys follow this structure:
`typescript
// Collections
[...keyPrefix, "collections", collectionName, "find", options]
[...keyPrefix, "collections", collectionName, "findOne", options]// Globals
[...keyPrefix, "globals", globalName, "get", options]
// Custom
[...keyPrefix, ...customKey]
`TypeScript
Full type inference from your CMS schema:
`typescript
import type { CollectionSelect } from "questpie";
import type { AppCMS } from "@/cms";// Collection item type
type Post = CollectionSelect;
// Query result types are automatically inferred
const { data } = useQuery(q.collections.posts.find({ limit: 10 }));
// ^? { docs: Post[], totalDocs: number, ... }
`Related Packages
questpie - Core CMS engine
- @questpie/hono - Hono adapter
- @questpie/elysia - Elysia adapter
- @questpie/next` - Next.js adapterMIT