Build, Test, and Push CFDM Docker Image / test (push) Failing after 1m51s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
117 lines
3.6 KiB
Plaintext
117 lines
3.6 KiB
Plaintext
---
|
|
description: "React SPA with TanStack Router v1 + TanStack Query v5 — the definitive pattern for zero-loading-spinner routing, type-safe URLs, and cache-first data"
|
|
globs: ["src/routes/**/*", "src/queries/**/*", "src/lib/router.ts", "src/lib/queryClient.ts"]
|
|
alwaysApply: false
|
|
---
|
|
You are an expert in React, TanStack Router v1, TanStack Query v5, TypeScript, and Vite.
|
|
|
|
## Architecture
|
|
- TanStack Router: routing, URL state, navigation
|
|
- TanStack Query: server state, caching, mutations
|
|
- Loader = bridge: prefetches into Query cache before render → zero loading spinners for route data
|
|
- Components are pure UI: read from Query cache, trigger mutations
|
|
|
|
## Setup
|
|
```ts
|
|
// src/lib/queryClient.ts
|
|
export const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { staleTime: 60_000 } },
|
|
})
|
|
|
|
// src/lib/router.ts
|
|
export const router = createRouter({
|
|
routeTree,
|
|
context: { queryClient },
|
|
defaultPreload: 'intent',
|
|
defaultPreloadStaleTime: 0,
|
|
})
|
|
|
|
declare module '@tanstack/react-router' {
|
|
interface Register { router: typeof router }
|
|
}
|
|
|
|
// src/main.tsx
|
|
<QueryClientProvider client={queryClient}>
|
|
<RouterProvider router={router} context={{ queryClient }} />
|
|
</QueryClientProvider>
|
|
```
|
|
|
|
## Query Definitions
|
|
```ts
|
|
// src/queries/posts.ts
|
|
export const postKeys = {
|
|
all: ['posts'] as const,
|
|
detail: (id: string) => [...postKeys.all, 'detail', id] as const,
|
|
list: (f?: PostFilters) => [...postKeys.all, 'list', f] as const,
|
|
}
|
|
|
|
export const postQueryOptions = (id: string) =>
|
|
queryOptions({ queryKey: postKeys.detail(id), queryFn: () => fetchPost(id) })
|
|
|
|
export const postsQueryOptions = (filters?: PostFilters) =>
|
|
queryOptions({ queryKey: postKeys.list(filters), queryFn: () => fetchPosts(filters) })
|
|
```
|
|
|
|
## Loader + Component (zero loading state)
|
|
```tsx
|
|
export const Route = createFileRoute('/posts/$postId')({
|
|
loader: ({ context: { queryClient }, params }) =>
|
|
queryClient.ensureQueryData(postQueryOptions(params.postId)),
|
|
component: PostDetail,
|
|
})
|
|
|
|
function PostDetail() {
|
|
const { postId } = Route.useParams()
|
|
const { data: post } = useQuery(postQueryOptions(postId)) // always in cache from loader
|
|
return <h1>{post!.title}</h1>
|
|
}
|
|
```
|
|
|
|
## Search Params → Query Key
|
|
```tsx
|
|
const searchSchema = z.object({ page: z.number().default(1), q: z.string().optional() })
|
|
|
|
export const Route = createFileRoute('/posts/')({
|
|
validateSearch: searchSchema,
|
|
loader: ({ context: { queryClient }, location: { search } }) =>
|
|
queryClient.ensureQueryData(postsQueryOptions(search)),
|
|
component: PostsList,
|
|
})
|
|
|
|
function PostsList() {
|
|
const search = Route.useSearch()
|
|
const { data } = useQuery(postsQueryOptions(search))
|
|
// ...
|
|
}
|
|
```
|
|
|
|
## Mutations
|
|
```tsx
|
|
const mutation = useMutation({
|
|
mutationFn: createPost,
|
|
onSuccess: (newPost) => {
|
|
queryClient.setQueryData(postKeys.detail(newPost.id), newPost) // warm cache
|
|
queryClient.invalidateQueries({ queryKey: postKeys.list() })
|
|
navigate({ to: '/posts/$postId', params: { postId: newPost.id } }) // instant — no spinner
|
|
},
|
|
})
|
|
```
|
|
|
|
## Hover Prefetching
|
|
```tsx
|
|
<Link
|
|
to="/posts/$postId"
|
|
params={{ postId: post.id }}
|
|
onMouseEnter={() => queryClient.prefetchQuery(postQueryOptions(post.id))}
|
|
>
|
|
{post.title}
|
|
</Link>
|
|
```
|
|
|
|
## Key Rules
|
|
- Always define `queryOptions` outside components — never inline inside `useQuery()`
|
|
- Never use `useEffect` for data fetching — use loaders or `useQuery`
|
|
- Search params are the single source of truth for filter/pagination state
|
|
- After mutations: `setQueryData` + `invalidateQueries` for instant UI feedback
|
|
- `declare module '@tanstack/react-router'` router registration is required for full type safety
|