Initial commit
This commit is contained in:
48
skills/tanstack-patterns/examples/INDEX.md
Normal file
48
skills/tanstack-patterns/examples/INDEX.md
Normal 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)
|
||||
271
skills/tanstack-patterns/examples/advanced-patterns.md
Normal file
271
skills/tanstack-patterns/examples/advanced-patterns.md
Normal 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)
|
||||
239
skills/tanstack-patterns/examples/query-patterns.md
Normal file
239
skills/tanstack-patterns/examples/query-patterns.md
Normal 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"
|
||||
});
|
||||
```
|
||||
224
skills/tanstack-patterns/examples/router-patterns.md
Normal file
224
skills/tanstack-patterns/examples/router-patterns.md
Normal 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
|
||||
220
skills/tanstack-patterns/examples/server-functions.md
Normal file
220
skills/tanstack-patterns/examples/server-functions.md
Normal 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>
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user