home / skills / gilbertopsantosjr / fullstacknextjs / gs-tanstack-react-query

gs-tanstack-react-query skill

/skills/gs-tanstack-react-query

This skill helps you manage TanStack React Query with clean architecture, returning DTOs and handling queries, mutations, and cache invalidation.

npx playbooks add skill gilbertopsantosjr/fullstacknextjs --skill gs-tanstack-react-query

Review the files below or copy the command above to add this skill to your agents.

Files (4)
SKILL.md
4.0 KB
---
name: gs-tanstack-react-query
description: TanStack React Query for data fetching with Clean Architecture. Queries return DTOs, mutations call server actions. Use when working with useQuery, useMutation, cache invalidation, or integrating ZSA server actions.
---

# TanStack React Query (Clean Architecture)

## Data Flow with DTOs

```
useServerActionQuery → Server Action → Use Case → DTO
```

All query responses are **DTOs**, not Entities. TypeScript types should reflect this.

## Core Hooks

```typescript
import {
  useServerActionQuery,
  useServerActionMutation,
  useServerActionInfiniteQuery,
} from '@saas4dev/core'

import { useQueryClient } from '@tanstack/react-query'
```

## Query with DTO Types

```typescript
import type { CategoryDTO } from '@/features/category'
import { listCategoriesAction, getCategoryAction } from '@/features/category'

// List query - returns CategoryDTO[]
const { data, isLoading } = useServerActionQuery(listCategoriesAction, {
  input: { status: 'active' },
  queryKey: ['categories', 'list', { status: 'active' }],
})

// data.items is CategoryDTO[]

// Single item query
const { data: category } = useServerActionQuery(getCategoryAction, {
  input: { id },
  queryKey: ['categories', 'detail', id],
})

// category is CategoryDTO
```

## Mutation with Invalidation

```typescript
import { createCategoryAction } from '@/features/category'

const queryClient = useQueryClient()

const mutation = useServerActionMutation(createCategoryAction, {
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['categories'] })
    toast.success('Category created')
  },
  onError: (error) => toast.error(error.message),
})

// Usage
mutation.mutate({ name: 'New Category' })
```

## Query Key Pattern

```typescript
// Hierarchical keys for precise invalidation
['categories']                           // All categories
['categories', 'list']                   // All lists
['categories', 'list', { status }]       // Filtered list
['categories', 'detail', id]             // Single item
```

### Query Key Factory

```typescript
export const categoryKeys = {
  all: ['categories'] as const,
  lists: () => [...categoryKeys.all, 'list'] as const,
  list: (filters: { status?: string }) => [...categoryKeys.lists(), filters] as const,
  details: () => [...categoryKeys.all, 'detail'] as const,
  detail: (id: string) => [...categoryKeys.details(), id] as const,
}
```

## Optimistic Update

```typescript
const mutation = useServerActionMutation(updateCategoryAction, {
  onMutate: async (newData) => {
    await queryClient.cancelQueries({ queryKey: ['categories', 'detail', newData.id] })
    const previous = queryClient.getQueryData<CategoryDTO>(['categories', 'detail', newData.id])

    queryClient.setQueryData<CategoryDTO>(
      ['categories', 'detail', newData.id],
      (old) => old ? { ...old, ...newData } : old
    )

    return { previous }
  },
  onError: (err, newData, context) => {
    if (context?.previous) {
      queryClient.setQueryData(['categories', 'detail', newData.id], context.previous)
    }
  },
  onSettled: (_, __, variables) => {
    queryClient.invalidateQueries({ queryKey: ['categories', 'detail', variables.id] })
  },
})
```

## Pagination

```typescript
const { data, fetchNextPage, hasNextPage } = useServerActionInfiniteQuery(
  listCategoriesAction,
  {
    queryKey: ['categories', 'list'],
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    initialPageParam: undefined,
    input: ({ pageParam }) => ({ cursor: pageParam, limit: 20 }),
  }
)

// data.pages[].items is CategoryDTO[]
```

## Best Practices

1. **Type with DTOs** - `useQueryData<CategoryDTO>()` not `Category`
2. **Actions only** - Never call Use Cases from components
3. **Hierarchical keys** - Invalidate broadly, fetch specifically
4. **Error handling** - Show domain exception messages
5. **Optimistic updates** - Always implement rollback

## References

- Web Client: `skills/nextjs-web-client/SKILL.md`
- Server Actions: `skills/nextjs-server-actions/SKILL.md`

Overview

This skill provides opinionated TanStack React Query integrations following Clean Architecture patterns. It wires server actions to React Query hooks so queries return DTOs and mutations call server actions, with built-in patterns for cache keys, invalidation, optimistic updates, and pagination. Use it to keep data fetching type-safe and aligned with domain boundaries.

How this skill works

Components use hooks like useServerActionQuery, useServerActionMutation, and useServerActionInfiniteQuery that call server actions. Query responses are transformed into DTO types; components never depend on domain entities or direct use case calls. The skill relies on hierarchical query keys and a QueryClient to handle invalidation, optimistic updates, and pagination.

When to use it

  • When you fetch data from server actions and want responses typed as DTOs
  • When using useQuery/useMutation patterns with TanStack React Query
  • When you need structured cache invalidation across lists and details
  • When implementing optimistic UI updates with rollback support
  • When adding cursor-based pagination with infinite queries

Best practices

  • Always type hooks with DTO types (e.g., CategoryDTO) instead of domain entities
  • Call only server actions from components—never invoke use cases directly
  • Design hierarchical query keys for broad invalidation and specific refetches
  • Implement optimistic updates with onMutate/onError/onSettled and store previous state for rollback
  • Invalidate parent keys (e.g., ['categories']) after mutations to keep lists fresh

Example use cases

  • List active categories with useServerActionQuery returning CategoryDTO[]
  • Fetch a single category detail with a key like ['categories','detail',id]
  • Create a category with useServerActionMutation and invalidate ['categories'] on success
  • Update a category optimistically: patch the detail cache, rollback on error, then invalidate
  • Implement infinite scrolling for categories with useServerActionInfiniteQuery and nextCursor

FAQ

Should I use entity types or DTOs with these hooks?

Use DTO types for all query and mutation results. Components should only depend on DTO shapes returned by server actions.

How should I structure query keys?

Use hierarchical keys (e.g., ['categories'], ['categories','list',{filters}], ['categories','detail',id]) so you can invalidate broadly or target specific data.