TanStack Query
Server state management with TanStack Query in DeesseJS
TanStack Query
TanStack Query (formerly React Query) is included in DeesseJS for powerful server state management, caching, and data synchronization.
Setup
TanStack Query comes pre-configured in DeesseJS. No additional setup required.
// Already configured in app/providers.tsx
import { QueryProvider } from '@deessejs/query'
export default function Providers({ children }) {
return <QueryProvider>{children}</QueryProvider>
}Basic Usage
Fetching Data
'use client'
import { useQuery } from '@deessejs/query'
export function PostsList() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('/api/posts')
return response.json()
},
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}With TypeScript
import { useQuery } from '@deessejs/query'
interface Post {
id: string
title: string
content: string
}
export function PostsList() {
const { data } = useQuery<Post[]>({
queryKey: ['posts'],
queryFn: fetchPosts,
})
// data is typed as Post[] | undefined
}DeesseJS Helpers
useCollection
Fetch all items from a collection:
import { useCollection } from '@deessejs/query'
export function PostsList() {
const { data: posts, isLoading } = useCollection('posts', {
where: { published: true },
orderBy: { createdAt: 'desc' },
})
return (
<div>
{posts?.map((post) => (
<article key={post.id}>{post.title}</article>
))}
</div>
)
}useItem
Fetch a single item:
import { useItem } from '@deessejs/query'
export function PostDetail({ id }: { id: string }) {
const { data: post, isLoading } = useItem('posts', id)
if (isLoading) return <Skeleton />
return (
<article>
<h1>{post?.title}</h1>
<div>{post?.content}</div>
</article>
)
}Infinite Queries
Paginated data:
import { useInfiniteCollection } from '@deessejs/query'
export function PostsList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteCollection('posts', {
where: { published: true },
pageSize: 10,
})
const posts = data?.pages.flat()
return (
<div>
{posts?.map((post) => (
<article key={post.id}>{post.title}</article>
))}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
)
}Mutations
Creating Data
import { useMutation, useQueryClient } from '@deessejs/query'
import { useCreateItem } from '@deessejs/query'
export function CreatePostForm() {
const createPost = useCreateItem('posts')
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
await createPost.mutateAsync({
title: formData.get('title'),
content: formData.get('content'),
})
}
return (
<form onSubmit={handleSubmit}>
<input name="title" />
<textarea name="content" />
<button disabled={createPost.isPending}>
{createPost.isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
)
}Updating Data
import { useUpdateItem } from '@deessejs/query'
export function EditPost({ id }: { id: string }) {
const { data: post } = useItem('posts', id)
const updatePost = useUpdateItem('posts')
const handleSubmit = async (data: any) => {
await updatePost.mutateAsync({ id, data })
}
return <PostForm initialData={post} onSubmit={handleSubmit} />
}Deleting Data
import { useDeleteItem } from '@deessejs/query'
export function DeletePostButton({ id }: { id: string }) {
const deletePost = useDeleteItem('posts')
const queryClient = useQueryClient()
const handleDelete = async () => {
await deletePost.mutateAsync(id)
queryClient.invalidateQueries({ queryKey: ['posts'] })
}
return (
<button onClick={handleDelete} disabled={deletePost.isPending}>
Delete
</button>
)
}Cache Management
Invalidating Cache
import { useQueryClient } from '@deessejs/query'
export function ActionButton() {
const queryClient = useQueryClient()
const handleAction = async () => {
await performAction()
// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['posts'] })
// Invalidate all queries matching a pattern
queryClient.invalidateQueries({
queryKey: ['posts'],
type: 'all',
})
}
}Setting Cache Data
export function OptimisticUpdate() {
const queryClient = useQueryClient()
const handleLike = async (postId: string) => {
// Optimistic update
queryClient.setQueryData(['posts', postId], (old) => ({
...old,
likes: old.likes + 1,
}))
try {
await likePost(postId)
} catch {
// Rollback on error
queryClient.invalidateQueries({ queryKey: ['posts', postId] })
}
}
}Advanced Features
Dependent Queries
export function UserPosts({ userId }: { userId: string }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Only run when user exists
})
if (!user || !posts) return <Loading />
return <div>{/* ... */}</div>
}Parallel Queries
export function Dashboard() {
const postsQuery = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
const usersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
const analyticsQuery = useQuery({
queryKey: ['analytics'],
queryFn: fetchAnalytics,
})
if (postsQuery.isLoading || usersQuery.isLoading || analyticsQuery.isLoading) {
return <Loading />
}
return (
<div>
<PostsStats data={postsQuery.data} />
<UsersStats data={usersQuery.data} />
<Analytics data={analyticsQuery.data} />
</div>
)
}Background Refetching
export function LiveStats() {
const { data } = useQuery({
queryKey: ['stats'],
queryFn: fetchStats,
refetchInterval: 5000, // Refetch every 5 seconds
refetchIntervalInBackground: true, // Continue when tab is inactive
})
return <div>Visitors: {data?.visitors}</div>
}Server Actions
Using Server Actions with TanStack Query
// app/actions/posts.ts
'use server'
import { db } from '@deessejs/db'
import { revalidatePath } from 'next/cache'
export async function getPosts() {
return await db.posts.findMany({
where: { published: true },
})
}
export async function createPost(data: { title: string; content: string }) {
const post = await db.posts.create({
data: { title: data.title, content: data.content, published: true },
})
revalidatePath('/posts')
return post
}// app/posts/page.tsx
import { useAction } from '@deessejs/query'
import { getPosts, createPost } from '@/app/actions/posts'
export default function PostsPage() {
const { data: posts } = useAction(getPosts)
const { mutate: createPost, isPending } = useAction(createPost, {
onSuccess: () => {
// Refetch posts after creation
refetch()
},
})
return (
<div>
<button
onClick={() =>
createPost({
title: 'New Post',
content: 'Content here...',
})
}
disabled={isPending}
>
Create Post
</button>
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}Best Practices
Query Keys
Use stable, hierarchical query keys:
// Good
queryKey: ['posts']
queryKey: ['posts', postId]
queryKey: ['posts', { filter, sort }]
// Avoid
queryKey: ['posts', Date.now()] // Unstable keySelectors
Use selectors to transform data:
const { data: postTitle } = useQuery({
queryKey: ['posts', postId],
queryFn: fetchPost,
select: (post) => post.title, // Only re-render when title changes
})Retry Configuration
Configure retry behavior:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
retry: 3, // Retry 3 times on failure
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff
})Next Steps
- Learn about Zustand
- Explore nuqs
- Return to State Management Overview