Initial commit
This commit is contained in:
11
.claude-plugin/plugin.json
Normal file
11
.claude-plugin/plugin.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# research
|
||||
|
||||
API research and documentation retrieval using Firecrawl and Context7, with multi-agent synthesis capabilities
|
||||
109
plugin.lock.json
Normal file
109
plugin.lock.json
Normal file
@@ -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": []
|
||||
}
|
||||
}
|
||||
199
skills/tanstack-patterns/SKILL.md
Normal file
199
skills/tanstack-patterns/SKILL.md
Normal file
@@ -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 <div>...</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
83
skills/tanstack-patterns/checklists/tanstack-checklist.md
Normal file
83
skills/tanstack-patterns/checklists/tanstack-checklist.md
Normal file
@@ -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
|
||||
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>
|
||||
});
|
||||
```
|
||||
43
skills/tanstack-patterns/reference/INDEX.md
Normal file
43
skills/tanstack-patterns/reference/INDEX.md
Normal file
@@ -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)
|
||||
98
skills/tanstack-patterns/reference/caching-strategy.md
Normal file
98
skills/tanstack-patterns/reference/caching-strategy.md
Normal file
@@ -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>(["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
|
||||
97
skills/tanstack-patterns/reference/multi-tenant.md
Normal file
97
skills/tanstack-patterns/reference/multi-tenant.md
Normal file
@@ -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 <div>...</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
67
skills/tanstack-patterns/reference/query-config.md
Normal file
67
skills/tanstack-patterns/reference/query-config.md
Normal file
@@ -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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
66
skills/tanstack-patterns/reference/router-config.md
Normal file
66
skills/tanstack-patterns/reference/router-config.md
Normal file
@@ -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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Outlet />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 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);
|
||||
},
|
||||
});
|
||||
```
|
||||
46
skills/tanstack-patterns/templates/auth-layout.tsx
Normal file
46
skills/tanstack-patterns/templates/auth-layout.tsx
Normal file
@@ -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 (
|
||||
<div className="flex min-h-screen">
|
||||
{/* TODO: Update layout structure */}
|
||||
<Sidebar user={session.user} />
|
||||
<div className="flex-1">
|
||||
<Header user={session.user} />
|
||||
<main className="p-6">
|
||||
<Outlet /> {/* Child routes render here */}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
skills/tanstack-patterns/templates/custom-hook.ts
Normal file
39
skills/tanstack-patterns/templates/custom-hook.ts
Normal file
@@ -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);
|
||||
30
skills/tanstack-patterns/templates/page-route.tsx
Normal file
30
skills/tanstack-patterns/templates/page-route.tsx
Normal file
@@ -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 (
|
||||
<div>
|
||||
{/* TODO: Build your page UI */}
|
||||
<h1 className="text-2xl font-bold">Page Title</h1>
|
||||
<div>{JSON.stringify(data)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
skills/tanstack-patterns/templates/root-route.tsx
Normal file
35
skills/tanstack-patterns/templates/root-route.tsx
Normal file
@@ -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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="min-h-screen bg-background">
|
||||
<Outlet /> {/* Child routes render here */}
|
||||
</div>
|
||||
{/* Dev tools (only in development) */}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
61
skills/tanstack-patterns/templates/server-function.ts
Normal file
61
skills/tanstack-patterns/templates/server-function.ts
Normal file
@@ -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 };
|
||||
});
|
||||
Reference in New Issue
Block a user