Initial commit

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

View File

@@ -0,0 +1,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)

View 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

View 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

View 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>
);
}
```

View 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);
},
});
```