From a0db8884402971c0715cbea7df6c5b44a0ee2fe5 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 18:29:26 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 11 + README.md | 3 + plugin.lock.json | 109 +++++++ skills/tanstack-patterns/SKILL.md | 199 +++++++++++++ .../checklists/tanstack-checklist.md | 83 ++++++ skills/tanstack-patterns/examples/INDEX.md | 48 ++++ .../examples/advanced-patterns.md | 271 ++++++++++++++++++ .../examples/query-patterns.md | 239 +++++++++++++++ .../examples/router-patterns.md | 224 +++++++++++++++ .../examples/server-functions.md | 220 ++++++++++++++ skills/tanstack-patterns/reference/INDEX.md | 43 +++ .../reference/caching-strategy.md | 98 +++++++ .../reference/multi-tenant.md | 97 +++++++ .../reference/query-config.md | 67 +++++ .../reference/router-config.md | 66 +++++ .../templates/auth-layout.tsx | 46 +++ .../templates/custom-hook.ts | 39 +++ .../templates/page-route.tsx | 30 ++ .../templates/root-route.tsx | 35 +++ .../templates/server-function.ts | 61 ++++ 20 files changed, 1989 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/tanstack-patterns/SKILL.md create mode 100644 skills/tanstack-patterns/checklists/tanstack-checklist.md create mode 100644 skills/tanstack-patterns/examples/INDEX.md create mode 100644 skills/tanstack-patterns/examples/advanced-patterns.md create mode 100644 skills/tanstack-patterns/examples/query-patterns.md create mode 100644 skills/tanstack-patterns/examples/router-patterns.md create mode 100644 skills/tanstack-patterns/examples/server-functions.md create mode 100644 skills/tanstack-patterns/reference/INDEX.md create mode 100644 skills/tanstack-patterns/reference/caching-strategy.md create mode 100644 skills/tanstack-patterns/reference/multi-tenant.md create mode 100644 skills/tanstack-patterns/reference/query-config.md create mode 100644 skills/tanstack-patterns/reference/router-config.md create mode 100644 skills/tanstack-patterns/templates/auth-layout.tsx create mode 100644 skills/tanstack-patterns/templates/custom-hook.ts create mode 100644 skills/tanstack-patterns/templates/page-route.tsx create mode 100644 skills/tanstack-patterns/templates/root-route.tsx create mode 100644 skills/tanstack-patterns/templates/server-function.ts diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..04f3c3d --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "research", + "description": "API research and documentation retrieval using Firecrawl and Context7, with multi-agent synthesis capabilities", + "version": "1.0.0", + "author": { + "name": "Grey Haven Studio" + }, + "skills": [ + "./skills/tanstack-patterns" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7da567a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# research + +API research and documentation retrieval using Firecrawl and Context7, with multi-agent synthesis capabilities diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..b43b55d --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,109 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:greyhaven-ai/claude-code-config:grey-haven-plugins/research", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "b3bffd9e1fab2ecee15cfa1bec100fa1b61bac4f", + "treeHash": "89dd07fce270d0d921de5a2a891fd36c9d887f3bb8309745ab7cf8e8e305c7b6", + "generatedAt": "2025-11-28T10:17:04.728062Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "research", + "description": "API research and documentation retrieval using Firecrawl and Context7, with multi-agent synthesis capabilities", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "b40766f40603bd6d8e6aba6fe61f8fcc68054adf96baf38555c4ab7440ef282e" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "5975e88997aa6a60d88f89b30e632b3d435b9d48f69f3dd129e823e78ecd3abd" + }, + { + "path": "skills/tanstack-patterns/SKILL.md", + "sha256": "2bd490214227f62052755ebbfd9ea48222dff31933b00ef58233384c09628ed2" + }, + { + "path": "skills/tanstack-patterns/checklists/tanstack-checklist.md", + "sha256": "7bb07854406175e05dc0142885ee7e1505c1942badd55d911d8f904ae8ff465b" + }, + { + "path": "skills/tanstack-patterns/examples/advanced-patterns.md", + "sha256": "40df56b26a5e02ecf4733029497b8352ffa232ae3decf71460f8a6372ea1ee9d" + }, + { + "path": "skills/tanstack-patterns/examples/query-patterns.md", + "sha256": "6ac4d217bab14a47267e56f26eecf2be528764352ed7c6eb22f23ad83f503706" + }, + { + "path": "skills/tanstack-patterns/examples/INDEX.md", + "sha256": "255a468e9a02da8193a396dffe1ab9daea10ace4980067c90d2b73e237423112" + }, + { + "path": "skills/tanstack-patterns/examples/router-patterns.md", + "sha256": "f78b5303a920c9ef878b5952b64e9cbcb9090e2da0483ce2ac3e7d8364e9ea6a" + }, + { + "path": "skills/tanstack-patterns/examples/server-functions.md", + "sha256": "ad0ec65c7769ffdf73a5b66f5cc478e92a189d4e37d79ef0aa8b586444f109cc" + }, + { + "path": "skills/tanstack-patterns/templates/root-route.tsx", + "sha256": "199f2cb0735917d0e1ed05479caefcf8831f27d6fb7be9eeec84946b25e81634" + }, + { + "path": "skills/tanstack-patterns/templates/server-function.ts", + "sha256": "d8c5a40a38e8db43fb3a5f8233b50017c5e73c90a1abb746060cb52cbc0bae40" + }, + { + "path": "skills/tanstack-patterns/templates/page-route.tsx", + "sha256": "a66080d09814ba4c9f7476597b55157c93f7834e61429c25fa19d819c07c25db" + }, + { + "path": "skills/tanstack-patterns/templates/auth-layout.tsx", + "sha256": "c4bb295962127ea9d5b8a170256318397da55e23c427931eda83e7e9e656967d" + }, + { + "path": "skills/tanstack-patterns/templates/custom-hook.ts", + "sha256": "d60dbeff75d5bb9c2c02b0291c3ec143f2ed60960765bb5e970aa636c62b620b" + }, + { + "path": "skills/tanstack-patterns/reference/query-config.md", + "sha256": "63b778c225e846492c2fd96138c158aa17f7ca85b86d28b36d2111af989332fe" + }, + { + "path": "skills/tanstack-patterns/reference/caching-strategy.md", + "sha256": "2ad1c6ca68f0957b7ae8bb16460b1258301acc01bf2332d92d007b4cd4f1d8d7" + }, + { + "path": "skills/tanstack-patterns/reference/multi-tenant.md", + "sha256": "a4cfdac5361daff03791edb103f3e921fc94bcc7c2654096da7cb988e3e7deca" + }, + { + "path": "skills/tanstack-patterns/reference/router-config.md", + "sha256": "5c0cdb6754b88cb71d3dea49bfde836c6b88fb44d5d0cc4711ab19b1cba47dcf" + }, + { + "path": "skills/tanstack-patterns/reference/INDEX.md", + "sha256": "850cb804818614298c4a4bb78b0faf2d06e208195143a621b513ca15755b4d10" + } + ], + "dirSha256": "89dd07fce270d0d921de5a2a891fd36c9d887f3bb8309745ab7cf8e8e305c7b6" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/tanstack-patterns/SKILL.md b/skills/tanstack-patterns/SKILL.md new file mode 100644 index 0000000..e60a319 --- /dev/null +++ b/skills/tanstack-patterns/SKILL.md @@ -0,0 +1,199 @@ +--- +name: grey-haven-tanstack-patterns +description: Apply Grey Haven's TanStack ecosystem patterns - Router file-based routing, Query data fetching with staleTime, and Start server functions. Use when building React applications with TanStack Start. +--- + +# Grey Haven TanStack Patterns + +Follow Grey Haven Studio's patterns for TanStack Start, Router, and Query in React 19 applications. + +## TanStack Stack Overview + +Grey Haven uses the complete TanStack ecosystem: +- **TanStack Start**: Full-stack React framework with server functions +- **TanStack Router**: Type-safe file-based routing with loaders +- **TanStack Query**: Server state management with caching +- **TanStack Table** (optional): Data grids and tables +- **TanStack Form** (optional): Type-safe form handling + +## Critical Patterns + +### 1. 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 +│ └── settings/ +│ └── index.tsx # /settings +└── users/ + ├── index.tsx # /users + └── $userId.tsx # /users/:userId (dynamic param) +``` + +**Key conventions**: +- `__root.tsx` - Root layout with QueryClient provider +- `_authenticated/` - Protected route groups (underscore prefix) +- `_layout.tsx` - Layout wrapper for route groups +- `$param.tsx` - Dynamic route parameters + +### 2. TanStack Query Defaults + +```typescript +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60000, // 1 minute default + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); +``` + +### 3. Query Key Patterns + +```typescript +// ✅ CORRECT - Specific to general +queryKey: ["user", userId] +queryKey: ["users", { tenantId, page: 1 }] +queryKey: ["organizations", orgId, "teams"] + +// ❌ WRONG +queryKey: [userId] // Missing resource type +queryKey: ["getUser", userId] // Don't include function name +queryKey: [{ id: userId }] // Object first is confusing +``` + +### 4. Server Functions with Multi-Tenant + +```typescript +// ALWAYS include tenant_id parameter +export const getUserById = createServerFn("GET", async ( + userId: string, + tenantId: string +) => { + 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; +}); +``` + +### 5. Route Loaders + +```typescript +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
...
; +} +``` + +### 6. Mutations with Cache Invalidation + +```typescript +const mutation = useMutation({ + mutationFn: (data: UserUpdate) => updateUser(userId, data), + // Always invalidate queries after mutation + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user", userId] }); + }, +}); +``` + +## Caching Strategy + +Grey Haven uses these staleTime defaults: + +| Data Type | staleTime | Use Case | +|-----------|-----------|----------| +| Auth data | 5 minutes | User sessions, tokens | +| User profiles | 1 minute | User details | +| Lists | 1 minute | Data tables, lists | +| Static/config | 10 minutes | Settings, configs | +| Realtime | 0 (always refetch) | Notifications | + +```typescript +const STALE_TIMES = { + auth: 5 * 60 * 1000, // 5 minutes + user: 1 * 60 * 1000, // 1 minute + list: 1 * 60 * 1000, // 1 minute + static: 10 * 60 * 1000, // 10 minutes + realtime: 0, // Always refetch +}; +``` + +## Supporting Documentation + +All supporting files are under 500 lines per Anthropic best practices: + +- **[examples/](examples/)** - Complete code examples + - [router-patterns.md](examples/router-patterns.md) - File-based routing, layouts, navigation + - [query-patterns.md](examples/query-patterns.md) - Queries, mutations, infinite queries + - [server-functions.md](examples/server-functions.md) - Creating and using server functions + - [advanced-patterns.md](examples/advanced-patterns.md) - Dependent queries, parallel queries, custom hooks + - [INDEX.md](examples/INDEX.md) - Examples navigation + +- **[reference/](reference/)** - Configuration references + - [router-config.md](reference/router-config.md) - Router setup and configuration + - [query-config.md](reference/query-config.md) - QueryClient configuration + - [caching-strategy.md](reference/caching-strategy.md) - Detailed caching patterns + - [multi-tenant.md](reference/multi-tenant.md) - Multi-tenant patterns with RLS + - [INDEX.md](reference/INDEX.md) - Reference navigation + +- **[templates/](templates/)** - Copy-paste ready templates + - [root-route.tsx](templates/root-route.tsx) - Root layout template + - [auth-layout.tsx](templates/auth-layout.tsx) - Protected layout template + - [page-route.tsx](templates/page-route.tsx) - Basic page route template + - [server-function.ts](templates/server-function.ts) - Server function template + - [custom-hook.ts](templates/custom-hook.ts) - Custom query hook template + +- **[checklists/](checklists/)** - Pre-PR validation + - [tanstack-checklist.md](checklists/tanstack-checklist.md) - TanStack patterns checklist + +## When to Apply This Skill + +Use this skill when: +- Building TanStack Start applications +- Implementing routing with TanStack Router +- Managing server state with TanStack Query +- Creating server functions for data fetching +- Optimizing query performance with caching +- Implementing multi-tenant data access +- Setting up authentication flows with route protection +- Building data-heavy React applications + +## Template Reference + +These patterns are from Grey Haven's production template: +- **cvi-template**: TanStack Start + Router + Query + React 19 + +## Critical Reminders + +1. **staleTime**: Default 60000ms (1 minute) for queries +2. **Query keys**: Specific to general (["user", userId], not [userId]) +3. **Server functions**: Always include tenant_id parameter +4. **Multi-tenant**: Filter by tenant_id in all server functions +5. **Loaders**: Use for server-side data fetching before render +6. **Mutations**: Invalidate queries after successful mutation +7. **Prefetching**: Use for performance on hover/navigation +8. **Error handling**: Always handle error state in queries +9. **RLS**: Server functions use RLS-enabled database connection +10. **File-based routing**: Underscore prefix (_) for route groups/layouts diff --git a/skills/tanstack-patterns/checklists/tanstack-checklist.md b/skills/tanstack-patterns/checklists/tanstack-checklist.md new file mode 100644 index 0000000..c17f11b --- /dev/null +++ b/skills/tanstack-patterns/checklists/tanstack-checklist.md @@ -0,0 +1,83 @@ +# TanStack Patterns Checklist + +**Use before creating PR for TanStack Start/Router/Query code.** + +## TanStack Router + +- [ ] File structure follows conventions (`__root.tsx`, `_layout.tsx`, `$param.tsx`) +- [ ] Root route includes QueryClient provider +- [ ] Protected routes use `beforeLoad` for auth checks +- [ ] Loaders use `loader` for data fetching +- [ ] Route params are type-safe +- [ ] Navigation uses type-safe `Link` component +- [ ] Redirects include return URL in search params + +## TanStack Query + +- [ ] QueryClient configured with Grey Haven defaults (staleTime: 60000) +- [ ] Query keys follow pattern: specific to general (["user", userId]) +- [ ] Query keys don't include function names +- [ ] staleTime set appropriately for data type +- [ ] Error states handled in components +- [ ] Loading states handled in components +- [ ] Mutations invalidate queries after success +- [ ] Optimistic updates use `onMutate` and rollback on error + +## Server Functions + +- [ ] All server functions include `tenant_id` parameter +- [ ] Server functions use correct HTTP method (GET/POST/DELETE) +- [ ] Multi-tenant isolation with `tenant_id` filtering +- [ ] Error handling with appropriate error messages +- [ ] Return types are type-safe +- [ ] Server functions used with TanStack Query hooks + +## Multi-Tenant + +- [ ] `tenant_id` included in all server functions +- [ ] `tenant_id` included in all query keys +- [ ] `useTenant()` hook used for consistent tenant access +- [ ] Queries use `enabled: !!tenantId` guard +- [ ] RLS policies applied when using RLS pattern +- [ ] Test cases verify tenant isolation + +## Caching Strategy + +- [ ] Auth data: 5 minutes staleTime +- [ ] User profiles: 1 minute staleTime +- [ ] Lists: 1 minute staleTime +- [ ] Static/config: 10 minutes staleTime +- [ ] Realtime: 0 staleTime (always refetch) +- [ ] Prefetching used for performance optimization + +## Performance + +- [ ] Prefetching on hover/navigation +- [ ] Parallel queries for independent data +- [ ] Dependent queries use `enabled` option +- [ ] Infinite queries for pagination +- [ ] Background refetching configured appropriately + +## Code Quality + +- [ ] Custom hooks created for reusable query logic +- [ ] Query logic separated from component logic +- [ ] Type-safe throughout (params, return types, mutations) +- [ ] DevTools enabled in development +- [ ] No console errors or warnings + +## Testing + +- [ ] Route loaders tested +- [ ] Server functions tested +- [ ] Query hooks tested +- [ ] Mutation logic tested +- [ ] Tenant isolation tested +- [ ] Error handling tested + +## Documentation + +- [ ] Complex query logic documented +- [ ] Custom hooks documented +- [ ] Server functions documented with JSDoc +- [ ] Route loaders documented diff --git a/skills/tanstack-patterns/examples/INDEX.md b/skills/tanstack-patterns/examples/INDEX.md new file mode 100644 index 0000000..601253b --- /dev/null +++ b/skills/tanstack-patterns/examples/INDEX.md @@ -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) diff --git a/skills/tanstack-patterns/examples/advanced-patterns.md b/skills/tanstack-patterns/examples/advanced-patterns.md new file mode 100644 index 0000000..e8c99c9 --- /dev/null +++ b/skills/tanstack-patterns/examples/advanced-patterns.md @@ -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 ; + + return ( +
+

{user.name}

+ {organization &&

Organization: {organization.name}

} +
+ ); +} +``` + +## 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 ; + + return ( +
+ + + +
+ ); +} +``` + +## 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 ; + + return ( +
+

{user.name}

+ +
+ ); +} +``` + +## 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 ( +
+ {notifications?.map((notif) => ( + + ))} +
+ ); +} +``` + +## 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
{user.name}
; +} + +function App() { + return ( + }> + + + ); +} +``` + +## 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(["users"]); + return users?.find(u => u.id === userId); + }, + staleTime: 60000, + }); + + return
{user?.name}
; +} +``` + +## 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
...
; +} +``` + +## 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) diff --git a/skills/tanstack-patterns/examples/query-patterns.md b/skills/tanstack-patterns/examples/query-patterns.md new file mode 100644 index 0000000..7f8a154 --- /dev/null +++ b/skills/tanstack-patterns/examples/query-patterns.md @@ -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 ; + if (error) return ; + if (!user) return ; + + return
{user.name}
; +} +``` + +## 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 ( +
{ + e.preventDefault(); + handleSubmit({ name: "Updated Name" }); + }}> + +
+ ); +} +``` + +## 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 ( +
+ {data?.pages.map((page, i) => ( +
+ {page.map((user) => ( + + ))} +
+ ))} + + +
+ ); +} +``` + +## 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 ( +
+ {users.map((user) => ( + handleMouseEnter(user.id)} + > + {user.name} + + ))} +
+ ); +} +``` + +## 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 ; + + if (error) { + return ( + +

Error loading data

+

{error.message}

+
+ ); + } + + return ; +} +``` + +## 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" +}); +``` diff --git a/skills/tanstack-patterns/examples/router-patterns.md b/skills/tanstack-patterns/examples/router-patterns.md new file mode 100644 index 0000000..e773e22 --- /dev/null +++ b/skills/tanstack-patterns/examples/router-patterns.md @@ -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 ( + +
+ {/* Child routes render here */} +
+ + +
+ ); +} +``` + +## 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 ( +
+ +
+
+
+ {/* Child routes render here */} +
+
+
+ ); +} +``` + +## 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 ( +
+

Dashboard

+ +
+ ); +} +``` + +## 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 ( +
+

{user.name}

+ +
+ ); +} +``` + +## Navigation + +```typescript +import { Link, useNavigate } from "@tanstack/react-router"; + +function Navigation() { + const navigate = useNavigate(); + + return ( + + ); +} +``` + +## 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 diff --git a/skills/tanstack-patterns/examples/server-functions.md b/skills/tanstack-patterns/examples/server-functions.md new file mode 100644 index 0000000..965203f --- /dev/null +++ b/skills/tanstack-patterns/examples/server-functions.md @@ -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
...
; +} +``` + +## 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
Welcome, {authData?.user.name}!
; +} +``` + +## 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
...
; +} +``` + +## 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 => { // Type-safe return + // ... +}); + +// Client usage (types inferred) +const mutation = useMutation({ + mutationFn: (data: UserUpdate) => updateUser(userId, data), + // TypeScript knows the return type is Promise +}); +``` diff --git a/skills/tanstack-patterns/reference/INDEX.md b/skills/tanstack-patterns/reference/INDEX.md new file mode 100644 index 0000000..c87a7ff --- /dev/null +++ b/skills/tanstack-patterns/reference/INDEX.md @@ -0,0 +1,43 @@ +# TanStack Patterns Reference + +Configuration and pattern references for TanStack ecosystem. + +## Available References + +### [router-config.md](router-config.md) +Router setup and configuration. +- File structure conventions +- Root route setup +- Route naming conventions +- beforeLoad vs loader + +### [query-config.md](query-config.md) +QueryClient configuration. +- Default configuration +- Query options reference +- Mutation options reference +- Per-query overrides +- DevTools setup + +### [caching-strategy.md](caching-strategy.md) +Detailed caching patterns. +- Grey Haven staleTime standards +- Fresh vs stale behavior +- Cache invalidation strategies +- Manual cache updates +- Cache persistence + +### [multi-tenant.md](multi-tenant.md) +Multi-tenant patterns with RLS. +- Server function patterns +- Query key patterns +- Tenant context hook +- Row Level Security +- Best practices + +## Quick Reference + +**Need router setup?** → [router-config.md](router-config.md) +**Need query config?** → [query-config.md](query-config.md) +**Need caching strategy?** → [caching-strategy.md](caching-strategy.md) +**Need multi-tenant patterns?** → [multi-tenant.md](multi-tenant.md) diff --git a/skills/tanstack-patterns/reference/caching-strategy.md b/skills/tanstack-patterns/reference/caching-strategy.md new file mode 100644 index 0000000..d0dd37c --- /dev/null +++ b/skills/tanstack-patterns/reference/caching-strategy.md @@ -0,0 +1,98 @@ +# TanStack Query Caching Strategy + +Detailed caching patterns for Grey Haven projects. + +## Grey Haven staleTime Standards + +```typescript +const STALE_TIMES = { + auth: 5 * 60 * 1000, // 5 minutes (auth data) + user: 1 * 60 * 1000, // 1 minute (user profiles) + list: 1 * 60 * 1000, // 1 minute (lists) + static: 10 * 60 * 1000, // 10 minutes (static/config data) + realtime: 0, // 0 (always refetch, e.g., notifications) +}; +``` + +## Cache Behavior + +### Fresh vs Stale + +```typescript +// Data is "fresh" for staleTime duration +const { data } = useQuery({ + queryKey: ["user", userId], + queryFn: () => getUserById(userId), + staleTime: 60000, // Fresh for 60 seconds +}); + +// During "fresh" period: +// - No background refetch +// - Instant return from cache +// - New component mounts get cached data immediately + +// After "stale" period: +// - Background refetch on mount +// - Cached data shown while refetching +// - UI updates when new data arrives +``` + +## Cache Invalidation + +```typescript +import { useQueryClient } from "@tanstack/react-query"; + +const queryClient = useQueryClient(); + +// Invalidate all user queries +queryClient.invalidateQueries({ queryKey: ["users"] }); + +// Invalidate specific user +queryClient.invalidateQueries({ queryKey: ["user", userId] }); + +// Invalidate and refetch immediately +queryClient.invalidateQueries({ + queryKey: ["users"], + refetchType: "active" // Only refetch active queries +}); +``` + +## Manual Cache Updates + +```typescript +// Update cache directly (optimistic update) +queryClient.setQueryData(["user", userId], (old: User) => ({ + ...old, + name: "New Name" +})); + +// Get cached data +const cachedUser = queryClient.getQueryData(["user", userId]); +``` + +## Cache Persistence + +TanStack Query cache is in-memory only. For persistence: + +```typescript +import { persistQueryClient } from "@tanstack/react-query-persist-client"; +import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; + +const persister = createSyncStoragePersister({ + storage: window.localStorage, +}); + +persistQueryClient({ + queryClient, + persister, + maxAge: 1000 * 60 * 60 * 24, // 24 hours +}); +``` + +## Best Practices + +1. **Use staleTime**: Always set appropriate staleTime +2. **Invalidate after mutations**: Use `onSuccess` to invalidate +3. **Specific keys**: Use specific query keys for targeted invalidation +4. **Prefetch**: Prefetch data on hover/navigation +5. **Background refetch**: Let queries refetch in background diff --git a/skills/tanstack-patterns/reference/multi-tenant.md b/skills/tanstack-patterns/reference/multi-tenant.md new file mode 100644 index 0000000..a58cac9 --- /dev/null +++ b/skills/tanstack-patterns/reference/multi-tenant.md @@ -0,0 +1,97 @@ +# Multi-Tenant Patterns with TanStack + +Multi-tenant isolation patterns for TanStack Start applications. + +## Server Function Pattern + +**ALWAYS include tenant_id parameter:** + +```typescript +// ✅ CORRECT +export const getUsers = createServerFn("GET", async (tenantId: string) => { + return await db.query.users.findMany({ + where: eq(users.tenant_id, tenantId), + }); +}); + +// ❌ WRONG - Missing tenant_id +export const getUsers = createServerFn("GET", async () => { + return await db.query.users.findMany(); +}); +``` + +## Query Key Pattern + +Include tenant_id in query keys: + +```typescript +// ✅ CORRECT +const { data } = useQuery({ + queryKey: ["users", tenantId], + queryFn: () => getUsers(tenantId), +}); + +// ❌ WRONG - Missing tenant_id in key +const { data } = useQuery({ + queryKey: ["users"], + queryFn: () => getUsers(tenantId), +}); +``` + +## Tenant Context Hook + +```typescript +// src/lib/hooks/use-tenant.ts +import { useQuery } from "@tanstack/react-query"; +import { getAuthenticatedUser } from "~/lib/server/functions/auth"; + +export function useTenant() { + const { data: authData } = useQuery({ + queryKey: ["auth", "current-user"], + queryFn: () => getAuthenticatedUser(), + staleTime: 5 * 60 * 1000, // 5 minutes + }); + + return { + tenantId: authData?.tenantId, + user: authData?.user, + }; +} +``` + +## Usage in Components + +```typescript +function UsersList() { + const { tenantId } = useTenant(); + + const { data: users } = useQuery({ + queryKey: ["users", tenantId], + queryFn: () => getUsers(tenantId!), + enabled: !!tenantId, // Only run when tenantId is available + }); + + return
...
; +} +``` + +## Row Level Security (RLS) + +With RLS, tenant_id filtering is automatic: + +```typescript +// Server function with RLS-enabled connection +export const getUsers = createServerFn("GET", async () => { + // Uses authenticated database connection + // RLS policies automatically filter by tenant_id + return await db.query.users.findMany(); +}); +``` + +## Best Practices + +1. **Always include tenant_id**: In server functions and query keys +2. **Use tenant context**: Create `useTenant()` hook for consistency +3. **Enable guards**: Use `enabled: !!tenantId` for queries +4. **RLS when possible**: Prefer RLS over manual filtering +5. **Test isolation**: Verify tenant isolation in tests diff --git a/skills/tanstack-patterns/reference/query-config.md b/skills/tanstack-patterns/reference/query-config.md new file mode 100644 index 0000000..8e34e27 --- /dev/null +++ b/skills/tanstack-patterns/reference/query-config.md @@ -0,0 +1,67 @@ +# TanStack Query Configuration + +QueryClient configuration reference for Grey Haven projects. + +## Default Configuration + +```typescript +import { QueryClient } from "@tanstack/react-query"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60000, // 1 minute + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); +``` + +## Configuration Options + +### Query Options + +| Option | Default | Description | +|--------|---------|-------------| +| `staleTime` | 60000ms | How long data stays fresh | +| `retry` | 1 | Number of retry attempts | +| `refetchOnWindowFocus` | false | Refetch when window gains focus | +| `refetchInterval` | false | Auto-refetch interval | +| `enabled` | true | Whether query runs automatically | + +### Mutation Options + +| Option | Default | Description | +|--------|---------|-------------| +| `retry` | 0 | Number of retry attempts | +| `onSuccess` | undefined | Success callback | +| `onError` | undefined | Error callback | +| `onSettled` | undefined | Always runs after success/error | + +## Per-Query Configuration + +```typescript +const { data } = useQuery({ + queryKey: ["user", userId], + queryFn: () => getUserById(userId), + staleTime: 300000, // Override default (5 minutes) + retry: 3, // Override default + refetchOnWindowFocus: true, // Override default +}); +``` + +## DevTools Setup + +```typescript +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +function RootComponent() { + return ( + + + + + ); +} +``` diff --git a/skills/tanstack-patterns/reference/router-config.md b/skills/tanstack-patterns/reference/router-config.md new file mode 100644 index 0000000..d87a6dc --- /dev/null +++ b/skills/tanstack-patterns/reference/router-config.md @@ -0,0 +1,66 @@ +# TanStack Router Configuration + +Router setup and configuration reference. + +## File Structure + +``` +src/routes/ +├── __root.tsx # Root layout (required) +├── index.tsx # Homepage +├── _layout/ # Route group (underscore prefix) +│ ├── _layout.tsx # Group layout +│ └── page.tsx # /page +└── $param.tsx # Dynamic route +``` + +## Root Route Setup + +```typescript +// src/routes/__root.tsx +import { Outlet, createRootRoute } from "@tanstack/react-router"; +import { QueryClientProvider } from "@tanstack/react-query"; + +export const Route = createRootRoute({ + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + ); +} +``` + +## Route Naming Conventions + +| File | URL | Description | +|------|-----|-------------| +| `index.tsx` | `/` | Homepage | +| `about.tsx` | `/about` | Static route | +| `_layout.tsx` | - | Layout wrapper (no URL) | +| `$userId.tsx` | `/:userId` | Dynamic param | +| `_authenticated/` | - | Route group (no URL) | + +## beforeLoad vs loader + +- **beforeLoad**: Auth checks, redirects, context setup +- **loader**: Data fetching + +```typescript +export const Route = createFileRoute("/_authenticated/_layout")({ + beforeLoad: async () => { + const session = await getSession(); + if (!session) throw redirect({ to: "/login" }); + return { session }; + }, +}); + +export const Route = createFileRoute("/dashboard")({ + loader: async ({ context }) => { + return await getDashboardData(context.session.tenantId); + }, +}); +``` diff --git a/skills/tanstack-patterns/templates/auth-layout.tsx b/skills/tanstack-patterns/templates/auth-layout.tsx new file mode 100644 index 0000000..522e58e --- /dev/null +++ b/skills/tanstack-patterns/templates/auth-layout.tsx @@ -0,0 +1,46 @@ +// Grey Haven Studio - Protected Route Layout Template +// Copy this template for _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"; + +// TODO: Update route path to match your file location +export const Route = createFileRoute("/_authenticated/_layout")({ + // Auth check runs before loading + beforeLoad: async ({ context }) => { + const session = await getSession(); + + // Redirect to login if not authenticated + if (!session) { + throw redirect({ + to: "/auth/login", + search: { + redirect: context.location.href, // Save redirect URL + }, + }); + } + + // Make session available to child routes + return { session }; + }, + component: AuthenticatedLayout, +}); + +function AuthenticatedLayout() { + const { session } = Route.useRouteContext(); + + return ( +
+ {/* TODO: Update layout structure */} + +
+
+
+ {/* Child routes render here */} +
+
+
+ ); +} diff --git a/skills/tanstack-patterns/templates/custom-hook.ts b/skills/tanstack-patterns/templates/custom-hook.ts new file mode 100644 index 0000000..7be6fd7 --- /dev/null +++ b/skills/tanstack-patterns/templates/custom-hook.ts @@ -0,0 +1,39 @@ +// Grey Haven Studio - Custom Query Hook Template +// Copy this template for reusable query logic + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +// TODO: Import your server functions +// import { getResource, updateResource } from "~/lib/server/functions/resources"; + +// TODO: Update hook name and parameter types +export function useResource(resourceId: string, tenantId: string) { + const queryClient = useQueryClient(); + + // Query for fetching data + const query = useQuery({ + queryKey: ["resource", resourceId], // Include resourceId in key + queryFn: () => getResource(resourceId, tenantId), + staleTime: 60000, // Grey Haven default: 1 minute + }); + + // Mutation for updating data + const updateMutation = useMutation({ + mutationFn: (data: ResourceUpdate) => updateResource(resourceId, data, tenantId), + onSuccess: (updatedResource) => { + // Update cache with new data + queryClient.setQueryData(["resource", resourceId], updatedResource); + }, + }); + + // Return simplified interface + return { + resource: query.data, + isLoading: query.isLoading, + error: query.error, + update: updateMutation.mutate, + isUpdating: updateMutation.isPending, + }; +} + +// Usage in component: +// const { resource, isLoading, update, isUpdating } = useResource(id, tenantId); diff --git a/skills/tanstack-patterns/templates/page-route.tsx b/skills/tanstack-patterns/templates/page-route.tsx new file mode 100644 index 0000000..077163c --- /dev/null +++ b/skills/tanstack-patterns/templates/page-route.tsx @@ -0,0 +1,30 @@ +// Grey Haven Studio - Page Route Template +// Copy this template for any page route + +import { createFileRoute } from "@tanstack/react-router"; +// TODO: Import your server functions +// import { getPageData } from "~/lib/server/functions/page"; + +// TODO: Update route path to match your file location +export const Route = createFileRoute("/_authenticated/page")({ + // Loader fetches data on server before rendering + loader: async ({ context }) => { + const tenantId = context.session.tenantId; + // TODO: Replace with your data fetching + // return await getPageData(tenantId); + return { data: "Replace me" }; + }, + component: PageComponent, +}); + +function PageComponent() { + const data = Route.useLoaderData(); // Type-safe loader data + + return ( +
+ {/* TODO: Build your page UI */} +

Page Title

+
{JSON.stringify(data)}
+
+ ); +} diff --git a/skills/tanstack-patterns/templates/root-route.tsx b/skills/tanstack-patterns/templates/root-route.tsx new file mode 100644 index 0000000..af976f8 --- /dev/null +++ b/skills/tanstack-patterns/templates/root-route.tsx @@ -0,0 +1,35 @@ +// Grey Haven Studio - TanStack Router Root Route Template +// Copy this template for __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 + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); + +export const Route = createRootRoute({ + component: RootComponent, +}); + +function RootComponent() { + return ( + +
+ {/* Child routes render here */} +
+ {/* Dev tools (only in development) */} + + +
+ ); +} diff --git a/skills/tanstack-patterns/templates/server-function.ts b/skills/tanstack-patterns/templates/server-function.ts new file mode 100644 index 0000000..0cba17e --- /dev/null +++ b/skills/tanstack-patterns/templates/server-function.ts @@ -0,0 +1,61 @@ +// Grey Haven Studio - Server Function Template +// Copy this template for creating server functions + +import { createServerFn } from "@tanstack/start"; +import { db } from "~/lib/server/db"; +// TODO: Import your database schema +// import { resources } from "~/lib/server/schema/resources"; +import { eq, and } from "drizzle-orm"; + +// GET server function (for fetching data) +// TODO: Update function name and return type +export const getResource = createServerFn("GET", async ( + resourceId: string, + tenantId: string // ALWAYS include tenant_id! +) => { + // TODO: Replace with your query + const resource = await db.query.resources.findFirst({ + where: and( + eq(resources.id, resourceId), + eq(resources.tenant_id, tenantId) // Multi-tenant isolation! + ), + }); + + if (!resource) { + throw new Error("Resource not found"); + } + + return resource; +}); + +// POST server function (for creating data) +// TODO: Update function name and parameters +export const createResource = createServerFn("POST", async ( + data: { name: string; description?: string }, + tenantId: string // ALWAYS include tenant_id! +) => { + // TODO: Replace with your insert + const resource = await db.insert(resources).values({ + ...data, + tenant_id: tenantId, // Include tenant_id in insert! + }).returning(); + + return resource[0]; +}); + +// DELETE server function (for deleting data) +// TODO: Update function name +export const deleteResource = createServerFn("DELETE", async ( + resourceId: string, + tenantId: string // ALWAYS include tenant_id! +) => { + // TODO: Replace with your delete + await db.delete(resources).where( + and( + eq(resources.id, resourceId), + eq(resources.tenant_id, tenantId) // Ensure tenant isolation! + ) + ); + + return { success: true }; +});