Claude Code Plugins

Community-maintained marketplace

Feedback

Server-Side + Client-Side Data Fetching with Orval + TanStack Query HydrationBoundary Pattern. ALWAYS use Orval - NEVER manual fetch()!

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name data-fetching
description Server-Side + Client-Side Data Fetching with Orval + TanStack Query HydrationBoundary Pattern. ALWAYS use Orval - NEVER manual fetch()!
allowed-tools Read, Edit, Write, Glob, Grep

Data Fetching Strategy (FSD)

Core Rule: ALWAYS use Orval-generated functions - NEVER manual fetch() calls!

FSD Paths

src/shared/
├── api/                        # Orval-generated
│   ├── endpoints/              # React Query Hooks
│   ├── models/                 # TypeScript Types
│   └── custom-fetch.ts         # Fetch Wrapper
└── lib/
    ├── query-client.ts         # getQueryClient()
    ├── auth-server/            # Server-only: getSession()
    └── auth-client/            # Client-safe: signIn, signOut

The HydrationBoundary Pattern (TanStack Recommended)

Server Component (prefetchQuery) → HydrationBoundary → Client Component (useQuery)

Advantages over initialData

  • Cleaner: No manual response mapping
  • Streaming Support: Supports React 18 Streaming
  • Correct Cache: Query cache is properly hydrated
  • Type-Safe: Better TypeScript integration

Setup

1. Query Client Helper (shared/lib/query-client.ts)

import {
  isServer,
  QueryClient,
  defaultShouldDehydrateQuery,
} from "@tanstack/react-query"

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000,
      },
      dehydrate: {
        // Include pending queries for streaming
        shouldDehydrateQuery: (query) =>
          defaultShouldDehydrateQuery(query) ||
          query.state.status === "pending",
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined = undefined

export function getQueryClient() {
  if (isServer) {
    return makeQueryClient()
  }
  if (!browserQueryClient) browserQueryClient = makeQueryClient()
  return browserQueryClient
}

2. Providers (shared/config/providers.tsx)

"use client"

import { QueryClientProvider } from "@tanstack/react-query"
import { getQueryClient } from "@shared/lib/query-client"

export function Providers({ children }: { children: ReactNode }) {
  const queryClient = getQueryClient()

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

Server Component Pattern

// app/(protected)/dashboard/page.tsx - SERVER COMPONENT
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
import { cookies } from "next/headers"
import { redirect } from "next/navigation"
import { getStats, getGetStatsQueryKey } from "@shared/api/endpoints/users/users"
import { getQueryClient } from "@shared/lib/query-client"
import { getSession } from "@shared/lib/auth-server"
import { StatsGrid } from "@features/stats"

export default async function DashboardPage() {
  // 1. Check session
  const session = await getSession()
  if (!session) redirect("/login")

  // 2. Get cookies for server fetch
  const cookieStore = await cookies()
  const cookieHeader = cookieStore
    .getAll()
    .map((c) => `${c.name}=${c.value}`)
    .join("; ")

  // 3. Prefetch with Orval function
  const queryClient = getQueryClient()
  await queryClient.prefetchQuery({
    queryKey: getGetStatsQueryKey(),
    queryFn: () =>
      getStats({
        headers: { Cookie: cookieHeader },
        cache: "no-store",
      }),
  })

  // 4. Wrap with HydrationBoundary
  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <StatsGrid />
    </HydrationBoundary>
  )
}

Client Component Pattern

// features/stats/ui/stats-grid.tsx - CLIENT COMPONENT
"use client"

import { useGetStats, usePostStats } from "@shared/api/endpoints/users/users"
import { useSSE } from "../model/use-sse"

export function StatsGrid() {
  // SSE for real-time updates
  useSSE()

  // Data is already hydrated - no initialData needed!
  const { data: response } = useGetStats()

  // Mutation Hook
  const { mutate: updateStats } = usePostStats()

  const stats = response?.status === 200 ? response.data : null

  return (
    <div>
      <p>Projects: {stats?.projectCount}</p>
      <button onClick={() => updateStats({ data: { field: "projects", delta: 1 } })}>
        +1
      </button>
    </div>
  )
}

Multiple Data Sources

// Server Component
export default async function DashboardPage() {
  const session = await getSession()
  if (!session) redirect("/login")

  const cookieStore = await cookies()
  const cookieHeader = cookieStore
    .getAll()
    .map((c) => `${c.name}=${c.value}`)
    .join("; ")

  const queryClient = getQueryClient()

  // Parallel prefetch
  await Promise.all([
    queryClient.prefetchQuery({
      queryKey: getGetStatsQueryKey(),
      queryFn: () => getStats({ headers: { Cookie: cookieHeader }, cache: "no-store" }),
    }),
    queryClient.prefetchQuery({
      queryKey: getGetNotificationsQueryKey(),
      queryFn: () => getNotifications({ headers: { Cookie: cookieHeader }, cache: "no-store" }),
    }),
  ])

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <StatsGrid />
      <NotificationList />
    </HydrationBoundary>
  )
}

FORBIDDEN: Manual Fetch Calls

// ❌ NEVER DO THIS:
async function getStats() {
  const res = await fetch("http://localhost:8080/api/v1/stats")
  return res.json()
}

// ✅ ALWAYS DO THIS (Orval function):
import { getStats, getGetStatsQueryKey } from "@shared/api/endpoints/users/users"

await queryClient.prefetchQuery({
  queryKey: getGetStatsQueryKey(),
  queryFn: () => getStats({ headers: { Cookie: cookieHeader } }),
})

When to Server-Side Prefetch?

Server-Side (prefetchQuery in Server Component):

  • Initial Page Load (SEO, no flicker)
  • Protected Pages (check session before render)
  • Critical "above-the-fold" content
  • Data that must be immediately visible

When Client-Side Only?

Client-Side Only (useQuery without prefetch):

  • After user interaction (click, form submit)
  • Lazy-loaded content (below the fold)
  • Pagination, Infinite Scroll
  • Data that doesn't need to be immediately visible

SSE + React Query Integration

// features/stats/model/use-sse.ts
"use client"

import { useQueryClient } from "@tanstack/react-query"
import { useEffect } from "react"
import { getGetStatsQueryKey } from "@shared/api/endpoints/users/users"

export function useSSE() {
  const queryClient = useQueryClient()

  useEffect(() => {
    const eventSource = new EventSource(
      `${process.env.NEXT_PUBLIC_API_URL}/api/v1/events`
    )

    eventSource.addEventListener("stats-updated", () => {
      queryClient.invalidateQueries({ queryKey: getGetStatsQueryKey() })
    })

    return () => eventSource.close()
  }, [queryClient])
}

Streaming (Optional)

For streaming without await:

export default function PostsPage() {
  const queryClient = getQueryClient()

  // No await - starts fetch, doesn't block
  queryClient.prefetchQuery({
    queryKey: getGetPostsQueryKey(),
    queryFn: () => getPosts(),
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts /> {/* useSuspenseQuery here for streaming */}
    </HydrationBoundary>
  )
}

Summary

┌─────────────────────────────────────────────────────────────┐
│                    SERVER COMPONENT                          │
│  1. Check session (getSession)                              │
│  2. Get cookies for auth                                    │
│  3. prefetchQuery with Orval function                       │
│  4. Wrap with HydrationBoundary                             │
└─────────────────────────────────────────────────────────────┘
                              ↓
                     dehydrate(queryClient)
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                    CLIENT COMPONENT                          │
│  1. useQuery() - Data is already there!                     │
│  2. useSSE() for real-time updates                          │
│  3. useMutation() for changes                               │
└─────────────────────────────────────────────────────────────┘