tanstack-comprehensive
npx skills add https://github.com/retrip-ai/agent-skills --skill tanstack-comprehensive
Agent 安装分布
Skill 文档
Tanstack Comprehensive
Complete guide to Tanstack Start, Router, and Query patterns for full-stack type-safe applications.
Overview
This skill covers the complete Tanstack ecosystem used in this project:
- Tanstack Router – File-based routing with type-safe navigation
- Tanstack Query – Data fetching, caching, and state management
- Tanstack Start – Server-side rendering and data loading
- tRPC Integration – Type-safe API calls
When to Apply
Reference these guidelines when:
- Creating or modifying routes
- Implementing data fetching (client or server-side)
- Working with loaders, queries, or mutations
- Handling navigation or URL parameters
- Integrating forms with server operations
- Optimizing data loading patterns
Quick Reference
Critical Patterns
File-Based Routing:
- Pages:
dashboard/index.tsxâ/dashboard - Layouts:
_authenticated.tsxâ Wraps child routes - Dynamic:
users/$id.tsxâ/users/123
Data Loading (Recommended Pattern):
// loader + pendingComponent + useSuspenseQuery
export const Route = createFileRoute('/page')({
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(
context.trpc.myData.queryOptions()
);
},
pendingComponent: MySkeleton,
component: MyPage,
});
function MyPage() {
const trpc = useTrpc();
const { data } = useSuspenseQuery(trpc.myData.queryOptions());
return <div>{data.name}</div>;
}
tRPC Integration:
// â
CORRECT
const { data } = useQuery(trpc.organizations.getCurrent.queryOptions());
// â WRONG - This method doesn't exist
const { data } = trpc.organizations.getCurrent.useQuery();
Navigation:
<Link to="/users/$id" params={{ id: '123' }}>User</Link>
Search Params (Avoid Re-renders):
// â
Read on demand
const router = useRouter();
const ref = router.latestLocation.search.ref;
// â Subscribes to all changes
const search = useSearch({ from: '__root__' });
const ref = search.ref;
Architecture
workspace (Tanstack Start)
â tRPC client
api-trpc (Hono + tRPC)
â Drizzle ORM
Database (PostgreSQL)
Key Principle: Never access database directly. Always use tRPC procedures.
References
Complete documentation with examples:
references/routing.md– File-based routing, navigation, params, loaders, authenticationreferences/data-fetching.md– Queries, mutations, tRPC integration, SSR patterns, Mastra queriesreferences/server-functions.md– Server-side operations, tRPC from workspace, error handling
To find specific patterns:
grep -l "loader" references/*.md
grep -l "useSuspenseQuery" references/*.md
grep -l "search params" references/*.md
Core Concepts
1. Routing Patterns
File Structure Determines URLs:
index.tsxâ Route root (e.g.,/dashboard)_layout.tsxâ Layout wrapper (doesn’t affect URL)$param.tsxâ Dynamic segment- Nested folders â Nested routes
Type-Safe Navigation:
// Link component
<Link to="/users/$id" params={{ id }}>User</Link>
// Programmatic
const navigate = useNavigate();
navigate({ to: '/dashboard', search: { tab: 'overview' } });
// In component
const { id } = Route.useParams(); // Type-safe
const { tab } = Route.useSearch(); // Type-safe
2. Data Loading Patterns
Recommended: loader + pendingComponent + useSuspenseQuery
Why this pattern?
- Skeleton shows immediately (no blank screen)
- No duplicate queries (loader preloads, component reads cache)
- Simpler component code (data always available)
- Better UX (immediate visual feedback)
export const Route = createFileRoute('/dashboard')({
loader: async ({ context }) => {
// Preload all data in parallel
await Promise.all([
context.queryClient.ensureQueryData(
context.trpc.organizations.getCurrent.queryOptions()
),
context.queryClient.ensureQueryData(
context.trpc.members.list.queryOptions()
),
]);
},
pendingComponent: DashboardSkeleton,
component: DashboardPage,
});
function DashboardPage() {
const trpc = useTrpc();
// Data guaranteed - no loading checks needed
const { data: org } = useSuspenseQuery(
trpc.organizations.getCurrent.queryOptions()
);
const { data: members } = useSuspenseQuery(
trpc.members.list.queryOptions()
);
return <div>{org.name} - {members.length} members</div>;
}
Anti-Pattern: useQuery + manual loading check
// â DON'T DO THIS
function MyPage() {
const { data, isLoading } = useQuery(...);
if (isLoading || !data) {
return <MySkeleton />; // Causes blank screen flash
}
return <div>{data.name}</div>;
}
3. tRPC Integration
CRITICAL: The project uses createTRPCOptionsProxy which provides factory functions, NOT hooks.
Correct Usage:
import { useQuery, useMutation } from '@tanstack/react-query';
import { trpc } from '@/trpc';
// Queries
const { data } = useQuery(
trpc.organizations.getCurrent.queryOptions()
);
// With params
const { data } = useQuery(
trpc.users.getById.queryOptions({ id: '123' })
);
// Mutations
const mutation = useMutation(
trpc.organizations.update.mutationOptions()
);
mutation.mutate({ name: 'New Name' });
Query Invalidation:
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
// Invalidate specific query
queryClient.invalidateQueries({
queryKey: [['organizations', 'getCurrent']],
});
// Invalidate all organization queries
queryClient.invalidateQueries({
queryKey: [['organizations']],
});
4. Search Params Optimization
Problem: useSearch() subscribes to ALL search param changes, causing unnecessary re-renders.
Solution: Read search params on-demand using router.latestLocation.
// â
CORRECT - No re-renders
import { useRouter } from '@tanstack/react-router';
function ShareButton({ chatId }: Props) {
const router = useRouter();
const handleShare = () => {
const ref = router.latestLocation.search.ref;
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}
// â WRONG - Re-renders on every search param change
import { useSearch } from '@tanstack/react-router';
function ShareButton({ chatId }: Props) {
const search = useSearch({ from: '__root__' });
const handleShare = () => {
const ref = search.ref;
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}
5. Route Loaders
Pre-load data before navigation:
export const Route = createFileRoute('/_authenticated/dashboard')({
loader: async ({ context }) => {
// Blocks navigation until data loaded
await context.queryClient.ensureQueryData(
context.trpc.organizations.getCurrent.queryOptions()
);
},
pendingComponent: DashboardSkeleton,
component: DashboardPage,
});
6. Protected Routes
import { createFileRoute, redirect } from '@tanstack/react-router';
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ context }) => {
const session = await getSession(context);
if (!session) {
throw redirect({
to: '/sign-in',
search: { redirect: location.href },
});
}
},
});
7. Mastra Queries
For AI chat data (messages, threads), use query functions instead of tRPC:
// src/lib/mastra-queries.ts
export const threadMessagesQueryOptions = (threadId: string) => ({
queryKey: ['mastra', 'messages', threadId] as const,
queryFn: async () => {
const client = createMastraClient();
const { messages } = await client.listThreadMessages(threadId, {
agentId: 'retripAgent',
});
return toAISdkV5Messages(messages);
},
});
// In component
const { data: messages } = useSuspenseQuery(
threadMessagesQueryOptions(threadId)
);
Best Practices
â Do:
Routing:
- Use file-based routing for all pages
- Leverage type-safe params and search
- Use
Linkcomponent for navigation - Protect routes with
beforeLoad - Validate search params with Zod
Data Fetching:
- Use
ensureQueryDatain loaders withpendingComponent - Use
useSuspenseQueryfor loader-prefetched data - Use
queryOptions()with TanStack Query hooks - Invalidate queries after mutations
- Handle errors gracefully
Performance:
- Read search params on-demand (not reactively)
- Preload routes before navigation
- Load data in parallel with
Promise.all() - Use optimistic updates for instant feedback
â Don’t:
Routing:
- Mix file-based and programmatic routing
- Use plain
<a>tags for internal navigation - Skip search param validation
- Create deeply nested layouts unnecessarily
Data Fetching:
- Use non-existent
.useQuery()method on trpc object - Use
useQuery+ manualif (!data)checks for loader data - Forget
pendingComponentwhen using loaders - Skip error handling
- Query database directly from workspace
- Use
anytypes
Performance:
- Subscribe to all search params when you only need one
- Skip route preloading on hover/focus
- Load data sequentially when it could be parallel
Common Patterns
Mutation with Invalidation
const queryClient = useQueryClient();
const mutation = useMutation({
...trpc.organizations.update.mutationOptions(),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [['organizations', 'getCurrent']],
});
toast.success('Updated successfully');
},
});
Optimistic Updates
const mutation = useMutation({
...trpc.apiKeys.delete.mutationOptions(),
onMutate: async (deletedId) => {
await queryClient.cancelQueries({
queryKey: [['apiKeys', 'list']],
});
const previous = queryClient.getQueryData([['apiKeys', 'list']]);
queryClient.setQueryData([['apiKeys', 'list']], (old: any) =>
old?.filter((key: any) => key.id !== deletedId)
);
return { previous };
},
onError: (err, deletedId, context) => {
queryClient.setQueryData([['apiKeys', 'list']], context?.previous);
},
});
Dependent Queries
const { data: user } = useQuery(
trpc.users.getById.queryOptions({ id: userId })
);
const { data: posts } = useQuery({
...trpc.posts.list.queryOptions({ authorId: user?.id }),
enabled: !!user,
});
Route Preloading
import { Link, useRouter } from '@tanstack/react-router';
function Navigation() {
const router = useRouter();
return (
<Link
to="/dashboard"
onMouseEnter={() => router.preloadRoute('/dashboard')}
onFocus={() => router.preloadRoute('/dashboard')}
>
Dashboard
</Link>
);
}
Related Skills
react-best-practices– Performance optimization patternsform-patterns– Form handling with React Hook Form
Version: 1.0.0 Last updated: 2026-01-14