DeesseJS

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 key

Selectors

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

On this page