Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:29:26 +08:00
commit a0db888440
20 changed files with 1989 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
# TanStack Patterns Examples
Complete code examples for TanStack Start, Router, and Query.
## Available Examples
### [router-patterns.md](router-patterns.md)
File-based routing, layouts, dynamic routes, and navigation.
- Root layout with QueryClient
- Protected route layouts with auth
- Page routes with loaders
- Dynamic routes with params
- Type-safe navigation
### [query-patterns.md](query-patterns.md)
Queries, mutations, infinite queries, and prefetching.
- Query basics with Grey Haven defaults
- Query key patterns (correct vs wrong)
- Mutations with optimistic updates
- Infinite queries for pagination
- Prefetching for performance
- Error handling
### [server-functions.md](server-functions.md)
Creating and using TanStack Start server functions.
- GET/POST/DELETE server functions
- Using server functions in components
- Auth context in server functions
- Multi-tenant isolation patterns
- RLS with server functions
### [advanced-patterns.md](advanced-patterns.md)
Dependent queries, parallel queries, and custom hooks.
- Dependent queries with `enabled`
- Parallel queries for dashboards
- Custom query hooks
- Query composition
- Background refetching
- Suspense mode
- Placeholders and initial data
- Query cancellation
## Quick Navigation
**Need routing?** → [router-patterns.md](router-patterns.md)
**Need data fetching?** → [query-patterns.md](query-patterns.md)
**Need server-side code?** → [server-functions.md](server-functions.md)
**Need advanced patterns?** → [advanced-patterns.md](advanced-patterns.md)

View File

@@ -0,0 +1,271 @@
# Advanced TanStack Query Patterns
Complete examples for dependent queries, parallel queries, and custom hooks.
## Dependent Queries
```typescript
function UserOrganization({ userId }: { userId: string }) {
// First query: get user
const { data: user } = useQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
});
// Second query: get organization (depends on user)
const { data: organization } = useQuery({
queryKey: ["organization", user?.organization_id],
queryFn: () => getOrganizationById(user!.organization_id),
enabled: !!user?.organization_id, // Only run if user exists
});
if (!user) return <LoadingSpinner />;
return (
<div>
<h2>{user.name}</h2>
{organization && <p>Organization: {organization.name}</p>}
</div>
);
}
```
## Parallel Queries
```typescript
function Dashboard() {
// Run multiple queries in parallel
const userQuery = useQuery({
queryKey: ["user", "current"],
queryFn: () => getCurrentUser(),
});
const statsQuery = useQuery({
queryKey: ["stats", "dashboard"],
queryFn: () => getDashboardStats(),
});
const recentQuery = useQuery({
queryKey: ["recent", "activity"],
queryFn: () => getRecentActivity(),
});
// All queries run simultaneously (parallel fetching)
const isLoading = userQuery.isLoading || statsQuery.isLoading || recentQuery.isLoading;
if (isLoading) return <LoadingSpinner />;
return (
<div>
<UserHeader user={userQuery.data} />
<StatsCards stats={statsQuery.data} />
<ActivityFeed activity={recentQuery.data} />
</div>
);
}
```
## Custom Query Hooks
```typescript
// src/lib/hooks/use-user.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getUserById, updateUser } from "~/lib/server/functions/users";
export function useUser(userId: string) {
const queryClient = useQueryClient();
const query = useQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
staleTime: 60000,
});
const updateMutation = useMutation({
mutationFn: (data: UserUpdate) => updateUser(userId, data),
onSuccess: (updatedUser) => {
queryClient.setQueryData(["user", userId], updatedUser);
},
});
return {
user: query.data,
isLoading: query.isLoading,
error: query.error,
update: updateMutation.mutate,
isUpdating: updateMutation.isPending,
};
}
```
```typescript
// Using custom hook
function UserProfile({ userId }: { userId: string }) {
const { user, isLoading, update, isUpdating } = useUser(userId);
if (isLoading) return <LoadingSpinner />;
return (
<div>
<h1>{user.name}</h1>
<button
onClick={() => update({ name: "New Name" })}
disabled={isUpdating}
>
{isUpdating ? "Updating..." : "Update"}
</button>
</div>
);
}
```
## Query Composition
```typescript
// Base hook for fetching user
function useUserQuery(userId: string) {
return useQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
staleTime: 60000,
});
}
// Composed hook with additional functionality
function useUserWithPermissions(userId: string) {
const userQuery = useUserQuery(userId);
const permissionsQuery = useQuery({
queryKey: ["user", userId, "permissions"],
queryFn: () => getUserPermissions(userId),
enabled: !!userQuery.data,
staleTime: 60000,
});
return {
user: userQuery.data,
permissions: permissionsQuery.data,
isLoading: userQuery.isLoading || permissionsQuery.isLoading,
error: userQuery.error || permissionsQuery.error,
};
}
```
## Background Refetching
```typescript
function RealtimeNotifications() {
const { data: notifications } = useQuery({
queryKey: ["notifications"],
queryFn: () => getNotifications(),
staleTime: 0, // Always stale
refetchInterval: 30000, // Refetch every 30 seconds
refetchIntervalInBackground: true, // Even when tab is not focused
});
return (
<div>
{notifications?.map((notif) => (
<NotificationItem key={notif.id} notification={notif} />
))}
</div>
);
}
```
## Suspense Mode
```typescript
import { Suspense } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";
function UserProfile({ userId }: { userId: string }) {
// Suspense mode - no loading state needed
const { data: user } = useSuspenseQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
});
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<UserProfile userId="123" />
</Suspense>
);
}
```
## Placeholders and Initial Data
```typescript
function UserProfile({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const { data: user } = useQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
// Placeholder data while loading
placeholderData: () => {
// Try to find user in list cache
const users = queryClient.getQueryData<User[]>(["users"]);
return users?.find(u => u.id === userId);
},
staleTime: 60000,
});
return <div>{user?.name}</div>;
}
```
## Query Cancellation
```typescript
import { useQuery } from "@tanstack/react-query";
function SearchResults({ query }: { query: string }) {
const { data, isLoading } = useQuery({
queryKey: ["search", query],
queryFn: async ({ signal }) => {
// Pass AbortSignal to fetch
const response = await fetch(`/api/search?q=${query}`, { signal });
return response.json();
},
staleTime: 60000,
// Query automatically cancelled when query key changes
});
return <div>...</div>;
}
```
## Key Patterns
### When to Use Custom Hooks
- Reusing query logic across components
- Combining multiple queries
- Adding business logic to queries
- Simplifying component code
### When to Use Dependent Queries
- Second query needs data from first query
- Use `enabled` option to control execution
### When to Use Parallel Queries
- Multiple independent data sources
- No dependencies between queries
- Want to show loading state for all together
### When to Use Suspense
- React 18+ with Suspense boundaries
- Want declarative loading states
- Component tree can suspend
### Performance Tips
- Use `placeholderData` for instant UI feedback
- Use `staleTime` to reduce unnecessary refetches
- Use `refetchInterval` for real-time updates
- Cancel queries when component unmounts (automatic)

View File

@@ -0,0 +1,239 @@
# TanStack Query Patterns
Complete examples for queries, mutations, and infinite queries.
## Query Basics
```typescript
import { useQuery } from "@tanstack/react-query";
import { getUserById } from "~/lib/server/functions/users";
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ["user", userId], // Array key for cache
queryFn: () => getUserById(userId),
staleTime: 60000, // Grey Haven default: 1 minute
// Data is "fresh" for 60 seconds, no refetch during this time
});
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <NotFound />;
return <div>{user.name}</div>;
}
```
## Query Key Patterns
Use consistent query key structure:
```typescript
// ✅ Good query keys (specific to general)
queryKey: ["user", userId] // Single user
queryKey: ["users", { tenantId, page: 1 }] // List with filters
queryKey: ["organizations", orgId, "teams"] // Nested resource
// ❌ Bad query keys (inconsistent, not cacheable)
queryKey: [userId] // Missing resource type
queryKey: ["getUser", userId] // Don't include function name
queryKey: [{ id: userId, type: "user" }] // Object first is confusing
```
## Mutations with Optimistic Updates
```typescript
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateUser } from "~/lib/server/functions/users";
function EditUserForm({ user }: { user: User }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: UserUpdate) => updateUser(user.id, data),
// Optimistic update (immediate UI feedback)
onMutate: async (newData) => {
// Cancel ongoing queries
await queryClient.cancelQueries({ queryKey: ["user", user.id] });
// Snapshot previous value
const previousUser = queryClient.getQueryData(["user", user.id]);
// Optimistically update cache
queryClient.setQueryData(["user", user.id], (old: User) => ({
...old,
...newData,
}));
return { previousUser };
},
// On error, rollback
onError: (err, newData, context) => {
queryClient.setQueryData(["user", user.id], context.previousUser);
},
// Always refetch after mutation
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["user", user.id] });
},
});
const handleSubmit = (data: UserUpdate) => {
mutation.mutate(data);
};
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit({ name: "Updated Name" });
}}>
<button disabled={mutation.isPending}>
{mutation.isPending ? "Saving..." : "Save"}
</button>
</form>
);
}
```
## Infinite Queries (Pagination)
```typescript
import { useInfiniteQuery } from "@tanstack/react-query";
import { listUsers } from "~/lib/server/functions/users";
function UsersList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ["users"],
queryFn: ({ pageParam = 0 }) => listUsers({
limit: 50,
offset: pageParam,
}),
getNextPageParam: (lastPage, allPages) => {
// Return next offset or undefined if no more pages
return lastPage.length === 50 ? allPages.length * 50 : undefined;
},
initialPageParam: 0,
staleTime: 60000,
});
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? "Loading more..."
: hasNextPage
? "Load More"
: "No more users"}
</button>
</div>
);
}
```
## Prefetching (Performance Optimization)
```typescript
import { useQueryClient } from "@tanstack/react-query";
import { getUserById } from "~/lib/server/functions/users";
function UsersList({ users }: { users: User[] }) {
const queryClient = useQueryClient();
// Prefetch user details on hover
const handleMouseEnter = (userId: string) => {
queryClient.prefetchQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
staleTime: 60000,
});
};
return (
<div>
{users.map((user) => (
<Link
key={user.id}
to="/users/$userId"
params={{ userId: user.id }}
onMouseEnter={() => handleMouseEnter(user.id)}
>
{user.name}
</Link>
))}
</div>
);
}
```
## Query Error Handling
```typescript
import { useQuery } from "@tanstack/react-query";
import { Alert } from "~/lib/components/ui/alert";
function DataComponent() {
const { data, error, isLoading } = useQuery({
queryKey: ["data"],
queryFn: () => fetchData(),
retry: 1, // Retry once on failure
staleTime: 60000,
});
if (isLoading) return <LoadingSpinner />;
if (error) {
return (
<Alert variant="destructive">
<h3>Error loading data</h3>
<p>{error.message}</p>
</Alert>
);
}
return <DataDisplay data={data} />;
}
```
## Key Patterns
### Query States
- `isLoading` - First time loading
- `isFetching` - Background refetch
- `isPending` - No cached data yet
- `isError` - Query failed
- `isSuccess` - Query succeeded
### Mutation States
- `isPending` - Mutation in progress
- `isSuccess` - Mutation succeeded
- `isError` - Mutation failed
### Cache Invalidation
```typescript
// Invalidate all user queries
queryClient.invalidateQueries({ queryKey: ["users"] });
// Invalidate specific user
queryClient.invalidateQueries({ queryKey: ["user", userId] });
// Refetch immediately
queryClient.invalidateQueries({
queryKey: ["users"],
refetchType: "active"
});
```

View File

@@ -0,0 +1,224 @@
# TanStack Router Patterns
Complete examples for file-based routing, layouts, and navigation.
## File-Based Routing Structure
```
src/routes/
├── __root.tsx # Root layout (wraps all routes)
├── index.tsx # Homepage (/)
├── _authenticated/ # Protected routes group (underscore prefix)
│ ├── _layout.tsx # Auth layout wrapper
│ ├── dashboard.tsx # /dashboard
│ ├── profile.tsx # /profile
│ └── settings/
│ ├── index.tsx # /settings
│ └── billing.tsx # /settings/billing
├── auth/
│ ├── login.tsx # /auth/login
│ └── signup.tsx # /auth/signup
└── users/
├── index.tsx # /users
└── $userId.tsx # /users/:userId (dynamic param)
```
## Root Layout (__root.tsx)
```typescript
// src/routes/__root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// Create QueryClient with Grey Haven defaults
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60000, // 1 minute default stale time
retry: 1,
refetchOnWindowFocus: false,
},
},
});
export const Route = createRootRoute({
component: RootComponent,
});
function RootComponent() {
return (
<QueryClientProvider client={queryClient}>
<div className="min-h-screen bg-background">
<Outlet /> {/* Child routes render here */}
</div>
<ReactQueryDevtools initialIsOpen={false} />
<TanStackRouterDevtools position="bottom-right" />
</QueryClientProvider>
);
}
```
## Route Layouts (_layout.tsx)
```typescript
// src/routes/_authenticated/_layout.tsx
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
import { Header } from "~/lib/components/layout/Header";
import { Sidebar } from "~/lib/components/layout/Sidebar";
import { getSession } from "~/lib/server/functions/auth";
export const Route = createFileRoute("/_authenticated/_layout")({
// Loader runs on server for data fetching
beforeLoad: async ({ context }) => {
const session = await getSession();
if (!session) {
throw redirect({
to: "/auth/login",
search: {
redirect: context.location.href,
},
});
}
return { session };
},
component: AuthenticatedLayout,
});
function AuthenticatedLayout() {
const { session } = Route.useRouteContext();
return (
<div className="flex min-h-screen">
<Sidebar user={session.user} />
<div className="flex-1">
<Header user={session.user} />
<main className="p-6">
<Outlet /> {/* Child routes render here */}
</main>
</div>
</div>
);
}
```
## Page Routes with Loaders
```typescript
// src/routes/_authenticated/dashboard.tsx
import { createFileRoute } from "@tanstack/react-router";
import { getDashboardData } from "~/lib/server/functions/dashboard";
import { DashboardStats } from "~/lib/components/dashboard/DashboardStats";
export const Route = createFileRoute("/_authenticated/dashboard")({
// Loader fetches data on server before rendering
loader: async ({ context }) => {
const tenantId = context.session.tenantId;
return await getDashboardData(tenantId);
},
component: DashboardPage,
});
function DashboardPage() {
const data = Route.useLoaderData(); // Type-safe loader data
return (
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<DashboardStats data={data} />
</div>
);
}
```
## Dynamic Routes ($param.tsx)
```typescript
// src/routes/users/$userId.tsx
import { createFileRoute } from "@tanstack/react-router";
import { getUserById } from "~/lib/server/functions/users";
import { UserProfile } from "~/lib/components/users/UserProfile";
export const Route = createFileRoute("/users/$userId")({
// Access route params in loader
loader: async ({ params, context }) => {
const { userId } = params;
const tenantId = context.session.tenantId;
return await getUserById(userId, tenantId);
},
component: UserPage,
});
function UserPage() {
const user = Route.useLoaderData();
const { userId } = Route.useParams(); // Also available in component
return (
<div>
<h1 className="text-2xl font-bold">{user.name}</h1>
<UserProfile user={user} />
</div>
);
}
```
## Navigation
```typescript
import { Link, useNavigate } from "@tanstack/react-router";
function Navigation() {
const navigate = useNavigate();
return (
<nav>
{/* Type-safe Link component */}
<Link to="/" className="...">
Home
</Link>
<Link
to="/users/$userId"
params={{ userId: "123" }}
className="..."
>
User Profile
</Link>
{/* Programmatic navigation */}
<button
onClick={() => {
navigate({
to: "/dashboard",
replace: true, // Replace history entry
});
}}
>
Go to Dashboard
</button>
</nav>
);
}
```
## Key Patterns
### Underscore Prefix for Groups
- `_authenticated/` - Route group (doesn't add to URL)
- `_layout.tsx` - Layout wrapper for group
### beforeLoad vs loader
- `beforeLoad` - Auth checks, redirects
- `loader` - Data fetching
### Type Safety
- Route params are type-safe
- Loader data is type-safe
- Navigation is type-safe

View File

@@ -0,0 +1,220 @@
# TanStack Start Server Functions
Complete examples for creating and using server functions.
## Creating Server Functions
```typescript
// src/lib/server/functions/users.ts
import { createServerFn } from "@tanstack/start";
import { db } from "~/lib/server/db";
import { users } from "~/lib/server/schema/users";
import { eq, and } from "drizzle-orm";
// GET server function (automatic caching)
export const getUserById = createServerFn("GET", async (
userId: string,
tenantId: string
) => {
// Server-side code with database access
const user = await db.query.users.findFirst({
where: and(
eq(users.id, userId),
eq(users.tenant_id, tenantId) // Multi-tenant isolation!
),
});
if (!user) {
throw new Error("User not found");
}
return user;
});
// POST server function (mutations)
export const createUser = createServerFn("POST", async (
data: { name: string; email: string },
tenantId: string
) => {
const user = await db.insert(users).values({
...data,
tenant_id: tenantId,
}).returning();
return user[0];
});
// DELETE server function
export const deleteUser = createServerFn("DELETE", async (
userId: string,
tenantId: string
) => {
await db.delete(users).where(
and(
eq(users.id, userId),
eq(users.tenant_id, tenantId)
)
);
return { success: true };
});
```
## Using Server Functions in Components
```typescript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getUserById, createUser, deleteUser } from "~/lib/server/functions/users";
function UserManagement({ tenantId }: { tenantId: string }) {
const queryClient = useQueryClient();
// Query using server function
const { data: user } = useQuery({
queryKey: ["user", "123"],
queryFn: () => getUserById("123", tenantId),
});
// Mutation using server function
const createMutation = useMutation({
mutationFn: (data: { name: string; email: string }) =>
createUser(data, tenantId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
const deleteMutation = useMutation({
mutationFn: (userId: string) => deleteUser(userId, tenantId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
return <div>...</div>;
}
```
## Server Functions with Auth Context
```typescript
// src/lib/server/functions/auth.ts
import { createServerFn } from "@tanstack/start";
import { getSession } from "~/lib/server/auth";
export const getAuthenticatedUser = createServerFn("GET", async () => {
const session = await getSession();
if (!session) {
throw new Error("Not authenticated");
}
// Automatically includes tenant_id from session
return {
user: session.user,
tenantId: session.tenantId,
};
});
```
```typescript
// Using in a component
function ProfilePage() {
const { data: authData } = useQuery({
queryKey: ["auth", "current-user"],
queryFn: () => getAuthenticatedUser(),
staleTime: 300000, // 5 minutes for auth data
});
return <div>Welcome, {authData?.user.name}!</div>;
}
```
## Multi-Tenant Server Functions
```typescript
// ALWAYS include tenant_id in server functions
export const listOrganizations = createServerFn("GET", async (tenantId: string) => {
return await db.query.organizations.findMany({
where: eq(organizations.tenant_id, tenantId),
});
});
// Use in component with tenant context
function OrganizationsList() {
const { tenantId } = useTenant(); // Custom hook for tenant context
const { data: orgs } = useQuery({
queryKey: ["organizations", tenantId],
queryFn: () => listOrganizations(tenantId),
staleTime: 60000,
});
return <div>...</div>;
}
```
## RLS with Server Functions
```typescript
// Server function uses RLS-enabled database connection
export const getUsers = createServerFn("GET", async () => {
// Uses authenticated database connection with RLS
// tenant_id automatically filtered by RLS policies
return await db.query.users.findMany();
});
```
## Key Patterns
### HTTP Methods
- **GET**: Read operations (automatic caching)
- **POST**: Create operations
- **PUT**: Update operations
- **DELETE**: Delete operations
### Multi-Tenant Isolation
Always include `tenant_id` parameter:
```typescript
export const someFunction = createServerFn("GET", async (
param: string,
tenantId: string // REQUIRED
) => {
// Filter by tenant_id
});
```
### Error Handling
```typescript
export const getUser = createServerFn("GET", async (userId: string) => {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
});
if (!user) {
throw new Error("User not found"); // Automatically returns 500
}
return user;
});
```
### Type Safety
Server functions are fully type-safe:
```typescript
// Server function
export const updateUser = createServerFn("POST", async (
userId: string,
data: UserUpdate // Type-safe parameter
): Promise<User> => { // Type-safe return
// ...
});
// Client usage (types inferred)
const mutation = useMutation({
mutationFn: (data: UserUpdate) => updateUser(userId, data),
// TypeScript knows the return type is Promise<User>
});
```