Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:38:57 +08:00
commit 74b7e35182
34 changed files with 20806 additions and 0 deletions

View File

@@ -0,0 +1,404 @@
---
name: api-integration
description: Integrate Apidog + OpenAPI specifications with your React app. Covers MCP server setup, type generation, and query layer integration. Use when setting up API clients, generating types from OpenAPI, or integrating with Apidog MCP.
---
# API Integration (Apidog + MCP)
Integrate OpenAPI specifications with your frontend using Apidog MCP for single source of truth.
## Goal
The AI agent always uses the latest API specification to generate types and implement features correctly.
## Architecture
```
Apidog (or Backend)
→ OpenAPI 3.0/3.1 Spec
→ MCP Server (apidog-mcp-server)
→ AI Agent reads spec
→ Generate TypeScript types
→ TanStack Query hooks
→ React Components
```
## Process
### 1. Expose OpenAPI from Apidog
**Option A: Remote URL**
- Export OpenAPI spec from Apidog
- Host at a URL (e.g., `https://api.example.com/openapi.json`)
**Option B: Local File**
- Export OpenAPI spec to file
- Place in project (e.g., `./api-spec/openapi.json`)
### 2. Wire MCP Server
```json
// .claude/mcp.json or settings
{
"mcpServers": {
"API specification": {
"command": "npx",
"args": [
"-y",
"apidog-mcp-server@latest",
"--oas=https://api.example.com/openapi.json"
]
}
}
}
```
**With Local File:**
```json
{
"mcpServers": {
"API specification": {
"command": "npx",
"args": [
"-y",
"apidog-mcp-server@latest",
"--oas=./api-spec/openapi.json"
]
}
}
}
```
**Multiple APIs:**
```json
{
"mcpServers": {
"Main API": {
"command": "npx",
"args": ["-y", "apidog-mcp-server@latest", "--oas=https://api.main.com/openapi.json"]
},
"Auth API": {
"command": "npx",
"args": ["-y", "apidog-mcp-server@latest", "--oas=https://api.auth.com/openapi.json"]
}
}
}
```
### 3. Generate Types & Client
Create `/src/api` directory for all API-related code:
```
/src/api/
├── types.ts # Generated from OpenAPI
├── client.ts # HTTP client (axios/fetch)
├── queries/ # TanStack Query hooks
│ ├── users.ts
│ ├── posts.ts
│ └── ...
└── mutations/ # TanStack Mutation hooks
├── users.ts
├── posts.ts
└── ...
```
**Option A: Hand-Written Types (Lightweight)**
```typescript
// src/api/types.ts
import { z } from 'zod'
// Define schemas from OpenAPI
export const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
createdAt: z.string().datetime(),
})
export type User = z.infer<typeof UserSchema>
export const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true })
export type CreateUserDTO = z.infer<typeof CreateUserSchema>
```
**Option B: Code Generation (Recommended for large APIs)**
```bash
# Using openapi-typescript
pnpm add -D openapi-typescript
npx openapi-typescript https://api.example.com/openapi.json -o src/api/types.ts
# Using orval
pnpm add -D orval
npx orval --input https://api.example.com/openapi.json --output src/api
```
### 4. Create HTTP Client
```typescript
// src/api/client.ts
import axios from 'axios'
import createAuthRefreshInterceptor from 'axios-auth-refresh'
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: {
'Content-Type': 'application/json',
},
})
// Request interceptor - add auth token
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Response interceptor - handle token refresh
const refreshAuth = async (failedRequest: any) => {
try {
const refreshToken = localStorage.getItem('refreshToken')
const response = await axios.post('/auth/refresh', { refreshToken })
const { accessToken } = response.data
localStorage.setItem('accessToken', accessToken)
failedRequest.response.config.headers.Authorization = `Bearer ${accessToken}`
return Promise.resolve()
} catch (error) {
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
window.location.href = '/login'
return Promise.reject(error)
}
}
createAuthRefreshInterceptor(apiClient, refreshAuth, {
statusCodes: [401],
pauseInstanceWhileRefreshing: true,
})
```
### 5. Build Query Layer
**Feature-based query organization:**
```typescript
// src/api/queries/users.ts
import { queryOptions } from '@tanstack/react-query'
import { apiClient } from '../client'
import { User, UserSchema } from '../types'
// Query key factory
export const usersKeys = {
all: ['users'] as const,
lists: () => [...usersKeys.all, 'list'] as const,
list: (filters: string) => [...usersKeys.lists(), { filters }] as const,
details: () => [...usersKeys.all, 'detail'] as const,
detail: (id: string) => [...usersKeys.details(), id] as const,
}
// API functions
async function fetchUsers(): Promise<User[]> {
const response = await apiClient.get('/users')
return z.array(UserSchema).parse(response.data)
}
async function fetchUser(id: string): Promise<User> {
const response = await apiClient.get(`/users/${id}`)
return UserSchema.parse(response.data)
}
// Query options
export function usersListQueryOptions() {
return queryOptions({
queryKey: usersKeys.lists(),
queryFn: fetchUsers,
staleTime: 30_000,
})
}
export function userQueryOptions(id: string) {
return queryOptions({
queryKey: usersKeys.detail(id),
queryFn: () => fetchUser(id),
staleTime: 60_000,
})
}
// Hooks
export function useUsers() {
return useQuery(usersListQueryOptions())
}
export function useUser(id: string) {
return useQuery(userQueryOptions(id))
}
```
**Mutations:**
```typescript
// src/api/mutations/users.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { apiClient } from '../client'
import { CreateUserDTO, User, UserSchema } from '../types'
import { usersKeys } from '../queries/users'
async function createUser(data: CreateUserDTO): Promise<User> {
const response = await apiClient.post('/users', data)
return UserSchema.parse(response.data)
}
export function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: createUser,
onSuccess: (newUser) => {
// Add to cache
queryClient.setQueryData(usersKeys.detail(newUser.id), newUser)
// Invalidate list
queryClient.invalidateQueries({ queryKey: usersKeys.lists() })
},
})
}
```
## Validation Strategy
**Always validate API responses:**
```typescript
import { z } from 'zod'
// Runtime validation
async function fetchUser(id: string): Promise<User> {
const response = await apiClient.get(`/users/${id}`)
try {
return UserSchema.parse(response.data)
} catch (error) {
console.error('API response validation failed:', error)
throw new Error('Invalid API response format')
}
}
```
**Or use safe parse:**
```typescript
const result = UserSchema.safeParse(response.data)
if (!result.success) {
console.error('Validation errors:', result.error.errors)
throw new Error('Invalid user data')
}
return result.data
```
## Error Handling
**Global error handling:**
```typescript
import { QueryCache } from '@tanstack/react-query'
const queryCache = new QueryCache({
onError: (error, query) => {
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
toast.error('Resource not found')
} else if (error.response?.status === 500) {
toast.error('Server error. Please try again.')
}
}
},
})
```
## Best Practices
1. **Single Source of Truth** - OpenAPI spec via MCP is authoritative
2. **Validate Responses** - Use Zod schemas for runtime validation
3. **Encapsulation** - Keep all API details in `/src/api`
4. **Type Safety** - Export types from generated/hand-written schemas
5. **Error Handling** - Handle auth errors, network errors, validation errors
6. **Query Key Factories** - Hierarchical keys for flexible invalidation
7. **Feature-Based Organization** - Group queries/mutations by feature
## Workflow with AI Agent
1. **Agent reads latest OpenAPI spec** via Apidog MCP
2. **Agent generates or updates** types in `/src/api/types.ts`
3. **Agent implements queries** following established patterns
4. **Agent creates mutations** with proper invalidation
5. **Agent updates components** to use new API hooks
## Example: Full Feature Implementation
```typescript
// 1. Types (generated or hand-written)
// src/api/types.ts
export const TodoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean(),
})
export type Todo = z.infer<typeof TodoSchema>
// 2. Queries
// src/api/queries/todos.ts
export const todosKeys = {
all: ['todos'] as const,
lists: () => [...todosKeys.all, 'list'] as const,
}
export function todosQueryOptions() {
return queryOptions({
queryKey: todosKeys.lists(),
queryFn: async () => {
const response = await apiClient.get('/todos')
return z.array(TodoSchema).parse(response.data)
},
})
}
// 3. Mutations
// src/api/mutations/todos.ts
export function useCreateTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (text: string) => {
const response = await apiClient.post('/todos', { text })
return TodoSchema.parse(response.data)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: todosKeys.lists() })
},
})
}
// 4. Component
// src/features/todos/TodoList.tsx
export function TodoList() {
const { data: todos } = useQuery(todosQueryOptions())
const createTodo = useCreateTodo()
return (
<div>
{todos?.map(todo => <TodoItem key={todo.id} {...todo} />)}
<AddTodoForm onSubmit={(text) => createTodo.mutate(text)} />
</div>
)
}
```
## Related Skills
- **tanstack-query** - Query and mutation patterns
- **tooling-setup** - TypeScript configuration for generated types
- **core-principles** - Project structure with `/src/api` directory

View File

@@ -0,0 +1,421 @@
---
name: api-spec-analyzer
description: Analyzes API documentation from OpenAPI specs to provide TypeScript interfaces, request/response formats, and implementation guidance. Use when implementing API integrations, debugging API errors (400, 401, 404), replacing mock APIs, verifying data types, or when user mentions endpoints, API calls, or backend integration.
---
# API Specification Analyzer
This Skill analyzes OpenAPI specifications to provide accurate API documentation, TypeScript interfaces, and implementation guidance for the caremaster-tenant-frontend project.
## When to use this Skill
Claude should invoke this Skill when:
- User is implementing a new API integration
- User encounters API errors (400 Bad Request, 401 Unauthorized, 404 Not Found, etc.)
- User wants to replace mock API with real backend
- User asks about data types, required fields, or API formats
- User mentions endpoints like "/api/users" or "/api/tenants"
- Before implementing any feature that requires API calls
- When debugging type mismatches between frontend and backend
## Instructions
### Step 1: Fetch API Documentation
Use the MCP server tools to get the OpenAPI specification:
```
mcp__Tenant_Management_Portal_API__read_project_oas_f4bjy4
```
If user requests fresh data or if documentation seems outdated:
```
mcp__Tenant_Management_Portal_API__refresh_project_oas_f4bjy4
```
For referenced schemas (when $ref is used):
```
mcp__Tenant_Management_Portal_API__read_project_oas_ref_resources_f4bjy4
```
### Step 2: Analyze the Specification
Extract the following information for each relevant endpoint:
1. **HTTP Method and Path**: GET /api/users, POST /api/tenants, etc.
2. **Authentication**: Bearer token, API key, etc.
3. **Request Parameters**:
- Path parameters (e.g., `:id`)
- Query parameters (e.g., `?page=1&limit=10`)
- Request body schema
- Required headers
4. **Response Specification**:
- Success response structure (200, 201, etc.)
- Error response formats (400, 401, 404, 500)
- Status codes and their meanings
5. **Data Types**:
- Exact types (string, number, boolean, array, object)
- Format specifications (ISO 8601, UUID, email)
- Required vs optional fields
- Enum values and constraints
- Default values
### Step 3: Generate TypeScript Interfaces
Create ready-to-use TypeScript interfaces that match the API specification exactly:
```typescript
/**
* User creation input
* Required fields: email, name, role
*/
export interface UserCreateInput {
/** User's email address - must be unique */
email: string
/** Full name of the user (2-100 characters) */
name: string
/** User role - determines access permissions */
role: "admin" | "manager" | "user"
/** Account status - defaults to "active" */
status?: "active" | "inactive"
}
/**
* User entity returned from API
*/
export interface User {
/** Unique identifier (UUID format) */
id: string
email: string
name: string
role: "admin" | "manager" | "user"
status: "active" | "inactive"
/** ISO 8601 timestamp */
createdAt: string
/** ISO 8601 timestamp */
updatedAt: string
}
```
### Step 4: Provide Implementation Guidance
#### API Service Pattern
```typescript
// src/api/userApi.ts
export async function createUser(input: UserCreateInput): Promise<User> {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${getToken()}`,
},
body: JSON.stringify(input),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.message)
}
return response.json()
}
```
#### TanStack Query Hook Pattern
```typescript
// src/hooks/useCreateUser.ts
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { createUser } from "@/api/userApi"
import { userKeys } from "@/lib/queryKeys"
import { toast } from "sonner"
export function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: createUser,
onSuccess: (newUser) => {
// Invalidate queries to refetch updated data
queryClient.invalidateQueries({ queryKey: userKeys.all() })
toast.success("User created successfully")
},
onError: (error) => {
toast.error(`Failed to create user: ${error.message}`)
},
})
}
```
#### Query Key Pattern
```typescript
// src/lib/queryKeys.ts
export const userKeys = {
all: () => ["users"] as const,
lists: () => [...userKeys.all(), "list"] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all(), "detail"] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
}
```
### Step 5: Document Security and Validation
- **OWASP Considerations**: SQL injection, XSS, CSRF protection
- **Input Validation**: Required field validation, format validation
- **Authentication**: Token handling, refresh logic
- **Error Handling**: Proper HTTP status code handling
- **Rate Limiting**: Retry logic, exponential backoff
### Step 6: Provide Test Recommendations
```typescript
// Example test cases based on API spec
describe("createUser", () => {
it("should create user with valid data", async () => {
// Test success case
})
it("should reject duplicate email", async () => {
// Test 409 Conflict
})
it("should validate email format", async () => {
// Test 400 Bad Request
})
it("should require authentication", async () => {
// Test 401 Unauthorized
})
})
```
## Output Format
Provide analysis in this structure:
```markdown
# API Analysis: [Endpoint Name]
## Endpoint Summary
- **Method**: POST
- **Path**: /api/users
- **Authentication**: Bearer token required
## Request Specification
### Path Parameters
None
### Query Parameters
None
### Request Body
[TypeScript interface]
### Required Headers
- Content-Type: application/json
- Authorization: Bearer {token}
## Response Specification
### Success Response (201)
[TypeScript interface]
### Error Responses
- 400: Validation error (duplicate email, invalid format)
- 401: Unauthorized (missing/invalid token)
- 403: Forbidden (insufficient permissions)
- 500: Server error
## Data Type Details
- **email**: string, required, must be valid email format, unique
- **name**: string, required, 2-100 characters
- **role**: enum ["admin", "manager", "user"], required
- **status**: enum ["active", "inactive"], optional, defaults to "active"
## TypeScript Interfaces
[Complete interfaces with JSDoc comments]
## Implementation Guide
[API service + TanStack Query hook examples]
## Security Notes
- Validate email format on client and server
- Hash passwords if handling credentials
- Use HTTPS for all requests
- Store tokens securely (httpOnly cookies recommended)
## Integration Checklist
- [ ] Add types to src/types/
- [ ] Create API service in src/api/
- [ ] Add query keys to src/lib/queryKeys.ts
- [ ] Create hooks in src/hooks/
- [ ] Add error handling with toast notifications
- [ ] Test with Vitest
```
## Project Conventions
### Path Aliases
Always use `@/` path alias:
```typescript
import { User } from "@/types/user"
import { createUser } from "@/api/userApi"
```
### Code Style (Biome)
- Tabs for indentation
- Double quotes
- Semicolons optional (only when needed)
- Line width: 100 characters
### File Organization
```
src/
├── types/ # Domain types
│ └── user.ts
├── api/ # API service functions
│ └── userApi.ts
├── hooks/ # TanStack Query hooks
│ └── useUsers.ts
└── lib/
└── queryKeys.ts # Query key factories
```
## Common Patterns
### Optimistic Updates
```typescript
onMutate: async (newUser) => {
// Cancel outgoing queries
await queryClient.cancelQueries({ queryKey: userKeys.lists() })
// Snapshot previous value
const previous = queryClient.getQueryData(userKeys.lists())
// Optimistically update cache
queryClient.setQueryData(userKeys.lists(), (old) => [...old, newUser])
return { previous }
},
onError: (err, newUser, context) => {
// Rollback on error
queryClient.setQueryData(userKeys.lists(), context.previous)
},
```
### Pagination
```typescript
export const userKeys = {
list: (page: number, limit: number) =>
[...userKeys.lists(), { page, limit }] as const,
}
```
### Search and Filters
```typescript
export interface UserFilters {
search?: string
role?: UserRole
status?: UserStatus
sortBy?: "name" | "email" | "createdAt"
sortOrder?: "asc" | "desc"
}
export const userKeys = {
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
}
```
## Error Handling Patterns
### API Service
```typescript
if (!response.ok) {
const error = await response.json()
throw new ApiError(error.message, response.status, error.details)
}
```
### Custom Hook
```typescript
onError: (error: ApiError) => {
if (error.status === 409) {
toast.error("Email already exists")
} else if (error.status === 400) {
toast.error("Invalid data: " + error.details)
} else {
toast.error("An error occurred. Please try again.")
}
}
```
## Quality Checklist
Before providing analysis, ensure:
- ✅ Fetched latest OpenAPI specification
- ✅ Extracted all required/optional fields
- ✅ Documented all possible status codes
- ✅ Created complete TypeScript interfaces
- ✅ Provided working code examples
- ✅ Noted security considerations
- ✅ Aligned with project conventions
- ✅ Included error handling patterns
## Examples
### Example 1: User asks to implement user creation
```
User: "I need to implement user creation"
Claude: [Invokes api-spec-analyzer Skill]
1. Fetches OpenAPI spec for POST /api/users
2. Extracts request/response schemas
3. Generates TypeScript interfaces
4. Provides API service implementation
5. Shows TanStack Query hook example
6. Lists validation requirements
```
### Example 2: User gets 400 error
```
User: "I'm getting a 400 error when creating a tenant"
Claude: [Invokes api-spec-analyzer Skill]
1. Fetches POST /api/tenants specification
2. Identifies required fields and formats
3. Compares user's implementation with spec
4. Points out data type mismatches
5. Provides corrected implementation
```
### Example 3: Replacing mock API
```
User: "Replace mockUserApi with real backend"
Claude: [Invokes api-spec-analyzer Skill]
1. Fetches all /api/users/* endpoints
2. Generates interfaces for all CRUD operations
3. Shows how to implement each API function
4. Maintains same interface as mock API
5. Provides migration checklist
```
## Notes
- Always fetch fresh documentation when user reports API issues
- Quote directly from OpenAPI spec when documenting requirements
- Flag ambiguities or missing information in documentation
- Prioritize type safety - use strict TypeScript types
- Follow existing patterns in the codebase
- Consider OWASP security guidelines
- Provide actionable, copy-paste-ready code

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,535 @@
---
name: browser-debugger
description: Systematically tests UI functionality, monitors console output, tracks network requests, and provides debugging reports using Chrome DevTools. Use after implementing UI features, when investigating console errors, for regression testing, or when user mentions testing, browser bugs, console errors, or UI verification.
allowed-tools: Task
---
# Browser Debugger
This Skill provides comprehensive browser-based UI testing and debugging capabilities using the tester agent and Chrome DevTools MCP server.
## When to use this Skill
Claude should invoke this Skill when:
- User has just implemented a UI feature and needs verification
- User reports console errors or warnings
- User wants to test form validation or user interactions
- User asks to verify API integration works in the browser
- After making significant code changes (regression testing)
- Before committing or deploying code
- User mentions: "test in browser", "check console", "verify UI", "does it work?"
- User describes UI bugs that need reproduction
## Instructions
### Phase 1: Understand Testing Scope
First, determine what needs to be tested:
1. **Default URL**: `http://localhost:5173` (caremaster-tenant-frontend dev server)
2. **Specific page**: If user mentions a route (e.g., "/users"), test that page
3. **Specific feature**: Focus testing on the mentioned feature
4. **Specific elements**: If user mentions buttons, forms, tables, test those
### Phase 2: Invoke tester Agent
Use the Task tool to launch the tester agent with comprehensive instructions:
```
Use Task tool with:
- subagent_type: "frontend:tester"
- prompt: [Detailed testing instructions below]
```
**Prompt structure for tester**:
```markdown
# Browser UI Testing Task
## Target
- URL: [http://localhost:5173 or specific page]
- Feature: [what to test]
- Goal: [verify functionality, check console, reproduce bug, etc.]
## Testing Steps
### Phase 1: Initial Assessment
1. Navigate to the URL using mcp__chrome-devtools__navigate_page or mcp__chrome-devtools__new_page
2. Take page snapshot using mcp__chrome-devtools__take_snapshot to see all interactive elements
3. Take screenshot using mcp__chrome-devtools__take_screenshot
4. Check baseline console state using mcp__chrome-devtools__list_console_messages
5. Check initial network activity using mcp__chrome-devtools__list_network_requests
### Phase 2: Systematic Interaction Testing
[If specific steps provided by user, list them here]
[Otherwise: Discovery mode - identify and test all interactive elements]
For each interaction:
**Before Interaction:**
1. Take screenshot: mcp__chrome-devtools__take_screenshot
2. Note current console message count
3. Identify element UID from snapshot
**Perform Interaction:**
- Click: mcp__chrome-devtools__click with element UID
- Fill: mcp__chrome-devtools__fill with element UID and value
- Hover: mcp__chrome-devtools__hover with element UID
**After Interaction:**
1. Wait 1-2 seconds for animations/transitions
2. Take screenshot: mcp__chrome-devtools__take_screenshot
3. Check console: mcp__chrome-devtools__list_console_messages
4. Check network: mcp__chrome-devtools__list_network_requests
5. Get details of any errors: mcp__chrome-devtools__get_console_message
6. Get details of failed requests: mcp__chrome-devtools__get_network_request
**Visual Analysis:**
Compare before/after screenshots:
- Did expected UI changes occur?
- Did modals appear/disappear?
- Did form submit successfully?
- Did error messages display?
- Did loading states show?
- Did content update?
### Phase 3: Console and Network Analysis
**Console Monitoring:**
1. List all console messages: mcp__chrome-devtools__list_console_messages
2. Categorize:
- Errors (critical - must fix)
- Warnings (should review)
- Info/debug messages
3. For each error:
- Get full details: mcp__chrome-devtools__get_console_message
- Note stack trace
- Identify which interaction triggered it
- Assess impact on functionality
**Network Monitoring:**
1. List all network requests: mcp__chrome-devtools__list_network_requests
2. Identify failed requests (4xx, 5xx status codes)
3. For each failure:
- Get request details: mcp__chrome-devtools__get_network_request
- Note request method, URL, status code
- Examine request/response payloads
- Determine cause (CORS, auth, validation, server error)
### Phase 4: Edge Case Testing
Test common failure scenarios:
**Form Validation:**
- Submit with empty required fields
- Submit with invalid data (bad email, short password)
- Verify error messages appear
- Verify form doesn't submit
**Error Handling:**
- Trigger known error conditions
- Verify error states display properly
- Check that app doesn't crash
**Loading States:**
- Verify loading indicators during async operations
- Check UI is disabled during loading
- Ensure loading clears after completion
**Console Cleanliness:**
- No React errors (missing keys, hook violations)
- No network errors (CORS, 404s, 500s)
- No deprecation warnings
- No unhandled promise rejections
## Required Output Format
Provide a comprehensive test report with this exact structure:
# Browser Debug Report
## Test Summary
- **Status**: [PASS / FAIL / PARTIAL]
- **URL Tested**: [url]
- **Test Duration**: [time in seconds]
- **Total Interactions**: [count]
- **Console Errors**: [count]
- **Console Warnings**: [count]
- **Failed Network Requests**: [count]
## Test Execution Details
### Step 1: [Action Description]
- **Action**: [What was done - e.g., "Clicked Create User button (UID: abc123)"]
- **Expected Result**: [What should happen]
- **Actual Result**: [What you observed in screenshots]
- **Visual Changes**: [Describe UI changes in detail]
- **Console Output**:
```
[New console messages, if any]
```
- **Network Activity**: [API calls triggered, if any]
- **Status**: ✓ PASS / ✗ FAIL
[Repeat for each test step]
## Console Analysis
### Critical Errors
[List each error with full details, stack trace, and impact assessment]
Or: ✓ No console errors detected
### Warnings
[List each warning with context and whether it should be fixed]
Or: ✓ No console warnings detected
### Info/Debug Messages
[Relevant informational output that helps understand behavior]
## Network Analysis
### Failed Requests
[For each failed request: method, URL, status, error message, payloads]
Or: ✓ All network requests successful
### Request Timeline
[List significant API calls with status codes and timing]
### Suspicious Activity
[Slow requests, repeated calls, unexpected endpoints]
## Visual Inspection Results
### UI Components Tested
- [Component 1]: ✓ Works as expected / ✗ Issue: [description]
- [Component 2]: ✓ Works as expected / ✗ Issue: [description]
[etc.]
### Visual Issues Found
[Layout problems, styling issues, alignment, broken images, responsive issues]
Or: ✓ No visual issues detected
## Issues Found
[If issues exist:]
### Critical Issues (Fix Immediately)
1. **[Issue Title]**
- **Description**: [Detailed description]
- **Steps to Reproduce**:
1. [Step 1]
2. [Step 2]
- **Expected**: [Expected behavior]
- **Actual**: [Actual behavior]
- **Error Messages**: [Console/network errors]
- **Impact**: [How this affects users]
- **Recommendation**: [How to fix]
### Minor Issues (Should Fix)
[Less critical but still important issues]
### Improvements (Nice to Have)
[Suggestions for better UX, performance, etc.]
[If no issues:]
✓ No issues found - all functionality working as expected
## Performance Notes
- Page load time: [if measured]
- Interaction responsiveness: [smooth / laggy / specific issues]
- Performance concerns: [any observations]
## Overall Assessment
[2-3 sentence summary of test results]
**Recommendation**: [DEPLOY / FIX CRITICAL ISSUES / NEEDS MORE WORK]
---
## Important Requirements
1. **Always analyze screenshots yourself** - describe what you see in detail
2. **Never return screenshots to the user** - only text descriptions
3. **Be specific** - "Modal appeared with title 'Create User'" not "Something happened"
4. **Document reproduction steps** for all issues
5. **Distinguish critical bugs from minor issues**
6. **Check console after EVERY interaction**
7. **Use exact element UIDs from snapshots**
8. **Wait for animations/transitions before checking results**
```
### Phase 3: Summarize Findings
After receiving the tester report:
1. **Present the test summary** to the user
2. **Highlight critical issues** that need immediate attention
3. **List console errors** with file locations
4. **Note failed network requests** with status codes
5. **Provide actionable recommendations** for fixes
6. **Suggest next steps** (fix bugs, commit code, deploy, etc.)
## Expected Test Report Structure
The tester will provide a detailed markdown report. Present it to the user in a clear, organized way:
```markdown
## 🧪 Browser Test Results
**Status**: [PASS/FAIL/PARTIAL] | **URL**: [url] | **Duration**: [time]
### Summary
- Total tests: [count]
- Console errors: [count]
- Failed requests: [count]
### Test Steps
[Summarized step-by-step results]
### Issues Found
**Critical** 🔴
- [Issue 1 with reproduction steps]
**Minor** 🟡
- [Issue 2]
### Console Errors
[List errors with file locations]
### Network Issues
[List failed requests with status codes]
### Recommendation
[DEPLOY / FIX FIRST / NEEDS WORK]
```
## Common Testing Scenarios
### Scenario 1: After Implementing Feature
User: "I just added user management"
**Your response:**
1. Invoke this Skill (automatically)
2. Test URL: http://localhost:5173/users
3. Test all CRUD operations
4. Verify console is clean
5. Check network requests succeed
6. Report results
### Scenario 2: Console Errors Reported
User: "I'm seeing errors in the console"
**Your response:**
1. Invoke this Skill
2. Navigate to the page
3. Capture all console messages
4. Get full error details with stack traces
5. Identify which interactions trigger errors
6. Provide detailed error analysis
### Scenario 3: Form Validation
User: "Test if the user form validation works"
**Your response:**
1. Invoke this Skill
2. Test empty form submission
3. Test invalid email format
4. Test short passwords
5. Test all validation rules
6. Verify error messages display correctly
### Scenario 4: Regression Testing
User: "I refactored the code, make sure nothing broke"
**Your response:**
1. Invoke this Skill
2. Test all major features
3. Check console for new errors
4. Verify all interactions still work
5. Compare with expected behavior
### Scenario 5: Pre-Commit Verification
User: "Ready to commit, verify everything works"
**Your response:**
1. Invoke this Skill
2. Run comprehensive smoke test
3. Check all features modified
4. Ensure console is clean
5. Verify no network failures
6. Give go/no-go recommendation
## Quality Checklist
Before completing testing, ensure:
- ✅ Tested all user-specified features
- ✅ Checked console for errors and warnings
- ✅ Monitored network requests
- ✅ Analyzed before/after screenshots
- ✅ Provided reproduction steps for issues
- ✅ Gave clear pass/fail status
- ✅ Made actionable recommendations
- ✅ Documented all findings clearly
## Chrome DevTools Integration
The tester agent has access to these Chrome DevTools MCP tools:
**Navigation:**
- `mcp__chrome-devtools__navigate_page` - Load URL
- `mcp__chrome-devtools__navigate_page_history` - Back/forward
- `mcp__chrome-devtools__new_page` - Open new page
**Inspection:**
- `mcp__chrome-devtools__take_snapshot` - Get page structure with UIDs
- `mcp__chrome-devtools__take_screenshot` - Capture visual state
- `mcp__chrome-devtools__list_pages` - List all open pages
**Interaction:**
- `mcp__chrome-devtools__click` - Click element by UID
- `mcp__chrome-devtools__fill` - Type into input by UID
- `mcp__chrome-devtools__fill_form` - Fill multiple fields at once
- `mcp__chrome-devtools__hover` - Hover over element
- `mcp__chrome-devtools__drag` - Drag and drop
- `mcp__chrome-devtools__wait_for` - Wait for text to appear
**Console:**
- `mcp__chrome-devtools__list_console_messages` - Get all console output
- `mcp__chrome-devtools__get_console_message` - Get detailed message
**Network:**
- `mcp__chrome-devtools__list_network_requests` - Get all requests
- `mcp__chrome-devtools__get_network_request` - Get request details
**Advanced:**
- `mcp__chrome-devtools__evaluate_script` - Run JavaScript
- `mcp__chrome-devtools__handle_dialog` - Handle alerts/confirms
- `mcp__chrome-devtools__performance_start_trace` - Start perf trace
- `mcp__chrome-devtools__performance_stop_trace` - Stop perf trace
## Project-Specific Considerations
### Tech Stack Awareness
**React 19 + TanStack Router:**
- Watch for React errors (missing keys, hook violations)
- Check for routing issues (404s, incorrect navigation)
**TanStack Query:**
- Monitor query cache invalidation
- Check for stale data issues
- Verify loading states
**Tailwind CSS:**
- Check responsive design
- Verify styling at different screen sizes
**Biome:**
- No impact on browser testing, but note code quality
### Common Issues to Watch For
**User Management:**
- CRUD operations work correctly
- Validation errors display
- Optimistic updates function
- Toast notifications appear
**API Integration:**
- Mock vs real API behavior differences
- Authentication token handling
- CORS issues
- 400/401/404 error handling
**Forms:**
- React Hook Form validation
- Submit button states
- Error message display
- Success feedback
## Tips for Effective Testing
1. **Be systematic**: Test one feature at a time
2. **Check console first**: Before AND after interactions
3. **Analyze screenshots carefully**: Describe what you see
4. **Get error details**: Don't just count errors, understand them
5. **Track network**: API failures are common issues
6. **Test edge cases**: Empty forms, invalid data
7. **Verify error states**: Errors should display gracefully
8. **Check loading states**: Async operations should show feedback
9. **Document clearly**: Reproduction steps are critical
10. **Prioritize issues**: Critical vs minor vs improvements
## Example Invocation
When user says: "I just implemented the user creation dialog"
**Your internal process:**
1. Recognize this needs browser testing (invoke browser-debugger Skill)
2. Determine test scope: user creation dialog functionality
3. Prepare comprehensive test instructions
4. Invoke tester agent via Task tool
5. Provide instructions to:
- Navigate to users page
- Click "Create User" button
- Take before/after screenshots
- Fill in form fields
- Test validation (empty, invalid)
- Submit form
- Verify user appears in table
- Check console for errors
- Check network for API call
- Report all findings
6. Receive detailed report from agent
7. Summarize findings for user
8. Recommend next steps
**Your response to user:**
"I'll test the user creation dialog functionality in the browser to verify everything works correctly and check for any console errors or issues."
[Invoke tester via Task tool with detailed instructions]
[After receiving report:]
"✅ Browser test completed successfully! The user creation dialog is working correctly. Here's what I verified:
**What Worked:**
- Create User button opens the dialog
- Form validation displays errors for empty fields
- Invalid email format is caught
- Successful submission adds user to table
- Success toast notification appears
**Console Check:**
- No errors detected
- No warnings present
**Network Check:**
- POST /api/users: 201 Created
- Response time: 145ms
**Recommendation**: Ready to commit! The feature is working as expected with no console errors or network issues."
## Notes
- Always check if dev server is running before testing
- Default to localhost:5173 for caremaster-tenant-frontend
- Provide actionable, specific findings
- Distinguish between critical bugs and minor issues
- Give clear recommendations (DEPLOY / FIX / NEEDS WORK)
- Be proactive: suggest testing after implementing features

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
---
name: core-principles
description: Core principles and project structure for React 19 SPA development. Covers stack overview, project organization, agent execution rules, and authoritative sources. Use when planning new projects, onboarding, or reviewing architectural decisions.
---
# Core Principles for React 19 SPA Development
Production-ready best practices for building modern React applications with TypeScript, Vite, and TanStack ecosystem.
## Stack Overview
- **React 19** with React Compiler (auto-memoization)
- **TypeScript** (strict mode)
- **Vite** (bundler)
- **Biome** (formatting + linting)
- **TanStack Query** (server state)
- **TanStack Router** (file-based routing)
- **Vitest** (testing with jsdom)
- **Apidog MCP** (API spec source of truth)
## Project Structure
```
/src
/app/ # App shell, providers, global styles
/routes/ # TanStack Router file-based routes
/components/ # Reusable, pure UI components (no data-fetch)
/features/ # Feature folders (UI + hooks local to a feature)
/api/ # Generated API types & client (from OpenAPI)
/lib/ # Utilities (zod schemas, date, formatting, etc.)
/test/ # Test utilities
```
**Key Principles:**
- One responsibility per file
- UI components don't fetch server data
- Put queries/mutations in feature hooks
- Co-locate tests next to files
## Agent Execution Rules
**Always do this when you add or modify code:**
1. **API Spec:** Fetch latest via Apidog MCP and regenerate `/src/api` types if changed
2. **Data Access:** Wire only through feature hooks that wrap TanStack Query. Never fetch inside UI components.
3. **New Routes:**
- Create file under `/src/routes/**` (file-based routing)
- If needs data at navigation, add loader that prefetches with Query
4. **Server Mutations:**
- Use React 19 Actions OR TanStack Query `useMutation` (choose one per feature)
- Use optimistic UI via `useOptimistic` (Actions) or Query's optimistic updates
- Invalidate/selectively update cache on success
5. **Compiler-Friendly:**
- Keep code pure (pure components, minimal effects)
- If compiler flags something, fix it or add `"use no memo"` temporarily
6. **Tests:**
- Add Vitest tests for new logic
- Component tests use RTL
- Stub network with msw
7. **Before Committing:**
- Run `biome check --write`
- Ensure Vite build passes
## "Done" Checklist per PR
- [ ] Route file added/updated; loader prefetch (if needed) present
- [ ] Query keys are stable (`as const`), `staleTime`/`gcTime` tuned
- [ ] Component remains pure; no unnecessary effects; compiler ✨ visible
- [ ] API calls typed from `/src/api`; inputs/outputs validated at boundaries
- [ ] Tests cover new logic; Vitest jsdom setup passes
- [ ] `biome check --write` clean; Vite build ok
## Authoritative Sources
- **React 19 & Compiler:**
- React v19 overview
- React Compiler: overview + installation + verification
- `<form action>` / Actions API; `useOptimistic`; `use`
- CRA deprecation & guidance
- **Vite:**
- Getting started; env & modes; TypeScript targets
- **TypeScript:**
- `moduleResolution: "bundler"` (for bundlers like Vite)
- **Biome:**
- Formatter/Linter configuration & CLI usage
- **TanStack Query:**
- Caching & important defaults; v5 migration notes; devtools/persisting cache
- **TanStack Router:**
- Install with Vite plugin; file-based routing; search params; devtools
- **Vitest:**
- Getting started & config (jsdom)
- **Apidog + MCP:**
- Apidog docs (import/export, OpenAPI); MCP server usage
## Final Notes
- Favor compile-friendly React patterns
- Let the compiler and Query/Router handle perf and data orchestration
- Treat Apidog's OpenAPI (via MCP) as the single source of truth for network shapes
- Keep this doc as your "contract"—don't add heavy frameworks or configs beyond what's here unless explicitly requested
## Related Skills
- **tooling-setup** - Vite, TypeScript, Biome configuration
- **react-patterns** - React 19 specific patterns (compiler, actions, forms)
- **tanstack-router** - Routing patterns
- **tanstack-query** - Server state management with Query v5
- **router-query-integration** - Integrating Router with Query
- **api-integration** - Apidog + MCP patterns
- **performance-security** - Performance, accessibility, security

View File

@@ -0,0 +1,415 @@
---
name: performance-security
description: Performance optimization, accessibility, and security best practices for React apps. Covers code-splitting, React Compiler patterns, asset optimization, a11y testing, and security hardening. Use when optimizing performance or reviewing security.
---
# Performance, Accessibility & Security
Production-ready patterns for building fast, accessible, and secure React applications.
## Performance Optimization
### Code-Splitting
**Automatic with TanStack Router:**
- File-based routing automatically code-splits by route
- Each route is its own chunk
- Vite handles dynamic imports efficiently
**Manual code-splitting:**
```typescript
import { lazy, Suspense } from 'react'
// Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'))
function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart data={data} />
</Suspense>
)
}
```
**Route-level lazy loading:**
```typescript
// src/routes/dashboard.lazy.tsx
export const Route = createLazyFileRoute('/dashboard')({
component: DashboardComponent,
})
```
### React Compiler First
The React Compiler automatically optimizes performance when you write compiler-friendly code:
**✅ Do:**
- Keep components pure (no side effects in render)
- Derive values during render (don't stash in refs)
- Keep props serializable
- Inline event handlers (unless they close over large objects)
**❌ Avoid:**
- Mutating props or state
- Side effects in render phase
- Over-using useCallback/useMemo (compiler handles this)
- Non-serializable props (functions, symbols)
**Verify optimization:**
- Check React DevTools for "Memo ✨" badge
- Components without badge weren't optimized (check for violations)
### Images & Assets
**Use Vite asset pipeline:**
```typescript
// Imports are optimized and hashed
import logo from './logo.png'
<img src={logo} alt="Logo" />
```
**Prefer modern formats:**
```typescript
// WebP for photos
<img src="/hero.webp" alt="Hero" />
// SVG for icons
import { ReactComponent as Icon } from './icon.svg'
<Icon />
```
**Lazy load images:**
```typescript
<img src={imageSrc} loading="lazy" alt="Description" />
```
**Responsive images:**
```typescript
<img
srcSet="
/image-320w.webp 320w,
/image-640w.webp 640w,
/image-1280w.webp 1280w
"
sizes="(max-width: 640px) 100vw, 640px"
src="/image-640w.webp"
alt="Description"
/>
```
### Bundle Analysis
```bash
# Build with analysis
npx vite build --mode production
# Visualize bundle
pnpm add -D rollup-plugin-visualizer
```
```typescript
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
react(),
visualizer({ open: true }),
],
})
```
### Performance Checklist
- [ ] Code-split routes and heavy components
- [ ] Verify React Compiler optimizations (✨ badges)
- [ ] Optimize images (WebP, lazy loading, responsive)
- [ ] Prefetch critical data in route loaders
- [ ] Use TanStack Query for automatic deduplication
- [ ] Set appropriate `staleTime` per query
- [ ] Minimize bundle size (check with visualizer)
- [ ] Enable compression (gzip/brotli on server)
## Accessibility (a11y)
### Semantic HTML
**✅ Use semantic elements:**
```typescript
// Good
<nav><a href="/about">About</a></nav>
<button onClick={handleClick}>Submit</button>
<main><article>Content</article></main>
// Bad
<div onClick={handleNav}>About</div>
<div onClick={handleClick}>Submit</div>
<div><div>Content</div></div>
```
### ARIA When Needed
**Only add ARIA when semantic HTML isn't enough:**
```typescript
// Custom select component
<div
role="listbox"
aria-label="Select country"
aria-activedescendant={activeId}
>
<div role="option" id="us">United States</div>
<div role="option" id="uk">United Kingdom</div>
</div>
// Loading state
<button aria-busy={isLoading} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Submit'}
</button>
```
### Keyboard Navigation
**Ensure all interactive elements are keyboard accessible:**
```typescript
function Dialog({ isOpen, onClose }: DialogProps) {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
if (isOpen) {
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}
}, [isOpen, onClose])
return isOpen ? (
<div role="dialog" aria-modal="true">
{/* Focus trap implementation */}
<button onClick={onClose} aria-label="Close dialog">×</button>
{/* Dialog content */}
</div>
) : null
}
```
### Testing with React Testing Library
**Use accessible queries (by role/label):**
```typescript
import { render, screen } from '@testing-library/react'
test('button is accessible', () => {
render(<button>Submit</button>)
// ✅ Good - query by role
const button = screen.getByRole('button', { name: /submit/i })
expect(button).toBeInTheDocument()
// ❌ Avoid - query by test ID
const button = screen.getByTestId('submit-button')
})
```
**Common accessible queries:**
```typescript
// By role (preferred)
screen.getByRole('button', { name: /submit/i })
screen.getByRole('textbox', { name: /email/i })
screen.getByRole('heading', { level: 1 })
// By label
screen.getByLabelText(/email address/i)
// By text
screen.getByText(/welcome/i)
```
### Color Contrast
- Ensure 4.5:1 contrast ratio for normal text
- Ensure 3:1 contrast ratio for large text (18pt+)
- Don't rely on color alone for meaning
- Test with browser DevTools accessibility panel
### Accessibility Checklist
- [ ] Use semantic HTML elements
- [ ] Add alt text to all images
- [ ] Ensure keyboard navigation works
- [ ] Provide focus indicators
- [ ] Test with screen reader (NVDA/JAWS/VoiceOver)
- [ ] Verify color contrast meets WCAG AA
- [ ] Use React Testing Library accessible queries
- [ ] Add skip links for main content
- [ ] Ensure form inputs have labels
## Security
### Never Ship Secrets
**❌ Wrong - secrets in code:**
```typescript
const API_KEY = 'sk_live_abc123' // Exposed in bundle!
```
**✅ Correct - environment variables:**
```typescript
// Only VITE_* variables are exposed to client
const API_KEY = import.meta.env.VITE_PUBLIC_KEY
```
**In `.env.local` (not committed):**
```bash
VITE_PUBLIC_KEY=pk_live_abc123 # Public key only!
```
**Backend handles secrets:**
```typescript
// Frontend calls backend, backend uses secret API key
await apiClient.post('/process-payment', { amount, token })
// Backend has access to SECRET_KEY via server env
```
### Validate All Untrusted Data
**At boundaries (API responses):**
```typescript
import { z } from 'zod'
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
async function fetchUser(id: string) {
const response = await apiClient.get(`/users/${id}`)
// Validate response
return UserSchema.parse(response.data)
}
```
**User input:**
```typescript
const formSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be 8+ characters'),
})
type FormData = z.infer<typeof formSchema>
function LoginForm() {
const handleSubmit = (data: unknown) => {
const result = formSchema.safeParse(data)
if (!result.success) {
setErrors(result.error.errors)
return
}
// result.data is typed and validated
login(result.data)
}
}
```
### XSS Prevention
React automatically escapes content in JSX:
```typescript
// ✅ Safe - React escapes
<div>{userInput}</div>
// ❌ Dangerous - bypasses escaping
<div dangerouslySetInnerHTML={{ __html: userInput }} />
```
**If you must use HTML:**
```typescript
import DOMPurify from 'dompurify'
<div dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(trustedHTML)
}} />
```
### Content Security Policy
Add CSP headers on server:
```nginx
# nginx example
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.example.com;
";
```
### Dependency Security
**Pin versions in package.json:**
```json
{
"dependencies": {
"react": "19.0.0", // Exact version
"@tanstack/react-query": "^5.59.0" // Allow patches
}
}
```
**Audit regularly:**
```bash
pnpm audit
pnpm audit --fix
```
**Use Renovate or Dependabot:**
```json
// .github/renovate.json
{
"extends": ["config:base"],
"automerge": true,
"major": { "automerge": false }
}
```
### CI Security
**Run with `--ignore-scripts`:**
```bash
# Prevents malicious post-install scripts
pnpm install --ignore-scripts
```
**Scan for secrets:**
```bash
# Add to CI
git-secrets --scan
```
### Security Checklist
- [ ] Never commit secrets or API keys
- [ ] Only expose `VITE_*` env vars to client
- [ ] Validate all API responses with Zod
- [ ] Sanitize user-generated HTML (if needed)
- [ ] Set Content Security Policy headers
- [ ] Pin dependency versions
- [ ] Run `pnpm audit` regularly
- [ ] Enable Renovate/Dependabot
- [ ] Use `--ignore-scripts` in CI
- [ ] Implement proper authentication flow
## Related Skills
- **core-principles** - Project structure and standards
- **react-patterns** - Compiler-friendly code
- **tanstack-query** - Performance via caching and deduplication
- **tooling-setup** - TypeScript strict mode for type safety

View File

@@ -0,0 +1,378 @@
---
name: react-patterns
description: React 19 specific patterns including React Compiler optimization, Server Actions, Forms, and new hooks. Use when implementing React 19 features, optimizing components, or choosing between Actions vs TanStack Query for mutations.
---
# React 19 Patterns and Best Practices
Modern React 19 patterns leveraging the React Compiler, Server Actions, and new hooks.
## Compiler-Friendly Code
The React Compiler automatically optimizes components for performance. Write code that works well with it:
**Best Practices:**
- Keep components pure and props serializable
- Derive values during render (don't stash in refs unnecessarily)
- Keep event handlers inline unless they close over large mutable objects
- Verify compiler is working (DevTools ✨ badge)
- Opt-out problematic components with `"use no memo"` while refactoring
**Example - Pure Component:**
```typescript
// ✅ Compiler-friendly - pure function
function UserCard({ user }: { user: User }) {
const displayName = `${user.firstName} ${user.lastName}`
const isVIP = user.points > 1000
return (
<div>
<h2>{displayName}</h2>
{isVIP && <Badge>VIP</Badge>}
</div>
)
}
// ❌ Avoid - unnecessary effects
function UserCard({ user }: { user: User }) {
const [displayName, setDisplayName] = useState('')
useEffect(() => {
setDisplayName(`${user.firstName} ${user.lastName}`)
}, [user])
return <div><h2>{displayName}</h2></div>
}
```
**Verification:**
- Open React DevTools
- Look for "Memo ✨" badge on components
- If missing, component wasn't optimized (check for violations)
**Opt-Out When Needed:**
```typescript
'use no memo'
// Component code that can't be optimized yet
function ProblematicComponent() {
// ... code with compiler issues
}
```
## Actions & Forms
For SPA mutations, choose **one approach per feature**:
- **React 19 Actions:** `<form action={fn}>`, `useActionState`, `useOptimistic`
- **TanStack Query:** `useMutation`
Don't duplicate logic between both approaches.
### React 19 Actions (Form-Centric)
**Best for:**
- Form submissions
- Simple CRUD operations
- When you want form validation built-in
**Basic Action:**
```typescript
'use server' // Only if using SSR/RSC, omit for SPA
async function createTodoAction(formData: FormData) {
const text = formData.get('text') as string
// Validation
if (!text || text.length < 3) {
return { error: 'Text must be at least 3 characters' }
}
// API call
await api.post('/todos', { text })
// Revalidation happens automatically
return { success: true }
}
// Component
function TodoForm() {
return (
<form action={createTodoAction}>
<input name="text" required />
<button type="submit">Add Todo</button>
</form>
)
}
```
**With State (useActionState):**
```typescript
import { useActionState } from 'react'
function TodoForm() {
const [state, formAction, isPending] = useActionState(
createTodoAction,
{ error: null, success: false }
)
return (
<form action={formAction}>
{state.error && <ErrorMessage>{state.error}</ErrorMessage>}
<input name="text" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Adding...' : 'Add Todo'}
</button>
</form>
)
}
```
**With Optimistic Updates (useOptimistic):**
```typescript
import { useOptimistic } from 'react'
function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
initialTodos,
(state, newTodo: string) => [
...state,
{ id: `temp-${Date.now()}`, text: newTodo, completed: false }
]
)
async function handleSubmit(formData: FormData) {
const text = formData.get('text') as string
addOptimisticTodo(text)
await createTodoAction(formData)
}
return (
<>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.id.startsWith('temp-') ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
<form action={handleSubmit}>
<input name="text" required />
<button type="submit">Add</button>
</form>
</>
)
}
```
### TanStack Query Mutations (Preferred for SPAs)
**Best for:**
- Non-form mutations (e.g., button clicks)
- Complex optimistic updates with rollback
- Integration with existing Query cache
- More control over caching and invalidation
See **tanstack-query** skill for comprehensive mutation patterns.
**Quick Example:**
```typescript
import { useMutation, useQueryClient } from '@tanstack/react-query'
function useCre
ateTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (text: string) => api.post('/todos', { text }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
// Usage
function TodoForm() {
const createTodo = useCreateTodo()
return (
<form onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
createTodo.mutate(formData.get('text') as string)
}}>
<input name="text" required />
<button type="submit" disabled={createTodo.isPending}>
{createTodo.isPending ? 'Adding...' : 'Add Todo'}
</button>
</form>
)
}
```
## The `use` Hook
The `use` hook unwraps Promises and Context, enabling new patterns.
**With Promises:**
```typescript
import { use, Suspense } from 'react'
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise)
return <div>{user.name}</div>
}
// Usage
function App() {
const userPromise = fetchUser(1)
return (
<Suspense fallback={<Spinner />}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}
```
**With Context:**
```typescript
import { use, createContext } from 'react'
const ThemeContext = createContext<string>('light')
function Button() {
const theme = use(ThemeContext)
return <button className={theme}>Click me</button>
}
```
**When to Use:**
- Primarily useful with Suspense/data primitives and RSC (React Server Components)
- **For SPA-only apps**, prefer **TanStack Query + Router loaders** for data fetching
- `use` shines when you already have a Promise from a parent component
## Component Composition Patterns
**Compound Components:**
```typescript
// ✅ Good - composable, flexible
<Card>
<Card.Header>
<Card.Title>Dashboard</Card.Title>
</Card.Header>
<Card.Content>
{/* content */}
</Card.Content>
</Card>
// Implementation
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>
}
Card.Header = function CardHeader({ children }: { children: React.ReactNode }) {
return <header className="card-header">{children}</header>
}
Card.Title = function CardTitle({ children }: { children: React.ReactNode }) {
return <h2 className="card-title">{children}</h2>
}
Card.Content = function CardContent({ children }: { children: React.ReactNode }) {
return <div className="card-content">{children}</div>
}
```
**Render Props (when needed):**
```typescript
function DataLoader<T>({
fetch,
render
}: {
fetch: () => Promise<T>
render: (data: T) => React.ReactNode
}) {
const { data } = useQuery({ queryKey: ['data'], queryFn: fetch })
if (!data) return <Spinner />
return <>{render(data)}</>
}
// Usage
<DataLoader
fetch={() => fetchUser(1)}
render={(user) => <UserCard user={user} />}
/>
```
## Error Boundaries
React 19 still requires class components for error boundaries (or use a library):
```typescript
import { Component, ReactNode } from 'react'
class ErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error: Error, info: { componentStack: string }) {
console.error('Error caught:', error, info)
}
render() {
if (this.state.hasError) {
return this.props.fallback
}
return this.props.children
}
}
// Usage
<ErrorBoundary fallback={<ErrorFallback />}>
<App />
</ErrorBoundary>
```
**Or use react-error-boundary library:**
```typescript
import { ErrorBoundary } from 'react-error-boundary'
<ErrorBoundary
fallback={<div>Something went wrong</div>}
onError={(error, info) => console.error(error, info)}
>
<App />
</ErrorBoundary>
```
## Decision Guide: Actions vs Query Mutations
| Scenario | Recommendation |
|----------|---------------|
| Form submission with validation | React Actions |
| Button click mutation | TanStack Query |
| Needs optimistic updates + rollback | TanStack Query |
| Integrates with existing cache | TanStack Query |
| SSR/RSC application | React Actions |
| SPA with complex data flow | TanStack Query |
| Simple CRUD with forms | React Actions |
**Rule of Thumb:** For SPAs with TanStack Query already in use, prefer Query mutations for consistency. Only use Actions for form-heavy features where the form-centric API is beneficial.
## Related Skills
- **tanstack-query** - Server state with mutations and optimistic updates
- **core-principles** - Overall project structure
- **tooling-setup** - React Compiler configuration

View File

@@ -0,0 +1,408 @@
---
name: router-query-integration
description: Integrate TanStack Router with TanStack Query for optimal data fetching. Covers route loaders with query prefetching, ensuring instant navigation, and eliminating request waterfalls. Use when setting up route loaders or optimizing navigation performance.
---
# Router × Query Integration
Seamlessly integrate TanStack Router with TanStack Query for optimal SPA performance and instant navigation.
## Route Loader + Query Prefetch
The key pattern: Use route loaders to prefetch queries BEFORE navigation completes.
**Benefits:**
- Loaders run before render, eliminating waterfall
- Fast SPA navigations (instant perceived performance)
- Queries still benefit from cache deduplication
- Add Router & Query DevTools during development (auto-hide in production)
## Basic Pattern
```typescript
// src/routes/users/$id.tsx
import { createFileRoute } from '@tanstack/react-router'
import { queryClient } from '@/app/queryClient'
import { usersKeys, fetchUser } from '@/features/users/queries'
export const Route = createFileRoute('/users/$id')({
loader: async ({ params }) => {
const id = params.id
return queryClient.ensureQueryData({
queryKey: usersKeys.detail(id),
queryFn: () => fetchUser(id),
staleTime: 30_000, // Fresh for 30 seconds
})
},
component: UserPage,
})
function UserPage() {
const { id } = Route.useParams()
const { data: user } = useQuery({
queryKey: usersKeys.detail(id),
queryFn: () => fetchUser(id),
})
// Data is already loaded from loader, so this returns instantly
return <div>{user.name}</div>
}
```
## Using Query Options Pattern (Recommended)
**Query Options** provide maximum type safety and DRY:
```typescript
// features/users/queries.ts
import { queryOptions } from '@tanstack/react-query'
export function userQueryOptions(userId: string) {
return queryOptions({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
staleTime: 30_000,
})
}
export function useUser(userId: string) {
return useQuery(userQueryOptions(userId))
}
// src/routes/users/$userId.tsx
import { userQueryOptions } from '@/features/users/queries'
import { queryClient } from '@/app/queryClient'
export const Route = createFileRoute('/users/$userId')({
loader: ({ params }) =>
queryClient.ensureQueryData(userQueryOptions(params.userId)),
component: UserPage,
})
function UserPage() {
const { userId } = Route.useParams()
const { data: user } = useUser(userId)
return <div>{user.name}</div>
}
```
## Multiple Queries in Loader
```typescript
export const Route = createFileRoute('/dashboard')({
loader: async () => {
// Run in parallel
await Promise.all([
queryClient.ensureQueryData(userQueryOptions()),
queryClient.ensureQueryData(statsQueryOptions()),
queryClient.ensureQueryData(postsQueryOptions()),
])
},
component: Dashboard,
})
function Dashboard() {
const { data: user } = useUser()
const { data: stats } = useStats()
const { data: posts } = usePosts()
// All data pre-loaded, renders instantly
return (
<div>
<UserHeader user={user} />
<StatsPanel stats={stats} />
<PostsList posts={posts} />
</div>
)
}
```
## Dependent Queries
```typescript
export const Route = createFileRoute('/users/$userId/posts')({
loader: async ({ params }) => {
// First ensure user data
const user = await queryClient.ensureQueryData(
userQueryOptions(params.userId)
)
// Then fetch user's posts
return queryClient.ensureQueryData(
userPostsQueryOptions(user.id)
)
},
component: UserPostsPage,
})
```
## Query Client Setup
**Export the query client for use in loaders:**
```typescript
// src/app/queryClient.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 0,
gcTime: 5 * 60_000,
retry: 1,
},
},
})
// src/main.tsx
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from './app/queryClient'
ReactDOM.createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode>
)
```
## Prefetch vs Ensure
**`prefetchQuery`** - Fire and forget, don't wait:
```typescript
loader: ({ params }) => {
// Don't await - just start fetching
queryClient.prefetchQuery(userQueryOptions(params.userId))
// Navigation continues immediately
}
```
**`ensureQueryData`** - Wait for data (recommended):
```typescript
loader: async ({ params }) => {
// Await - navigation waits until data is ready
return await queryClient.ensureQueryData(userQueryOptions(params.userId))
}
```
**`fetchQuery`** - Always fetches fresh:
```typescript
loader: async ({ params }) => {
// Ignores cache, always fetches
return await queryClient.fetchQuery(userQueryOptions(params.userId))
}
```
**Recommendation:** Use `ensureQueryData` for most cases - respects cache and staleTime.
## Handling Errors in Loaders
```typescript
export const Route = createFileRoute('/users/$userId')({
loader: async ({ params }) => {
try {
return await queryClient.ensureQueryData(userQueryOptions(params.userId))
} catch (error) {
// Let router error boundary handle it
throw error
}
},
errorComponent: ({ error }) => (
<div>
<h1>Failed to load user</h1>
<p>{error.message}</p>
</div>
),
component: UserPage,
})
```
## Invalidating Queries After Mutations
```typescript
// features/users/mutations.ts
export function useUpdateUser() {
const queryClient = useQueryClient()
const navigate = useNavigate()
return useMutation({
mutationFn: (user: UpdateUserDTO) => api.put(`/users/${user.id}`, user),
onSuccess: (updatedUser) => {
// Update cache immediately
queryClient.setQueryData(
userQueryOptions(updatedUser.id).queryKey,
updatedUser
)
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: ['users', 'list'] })
// Navigate to updated user page (will use cached data)
navigate({ to: '/users/$userId', params: { userId: updatedUser.id } })
},
})
}
```
## Preloading on Link Hover
```typescript
import { Link, useRouter } from '@tanstack/react-router'
function UserLink({ userId }: { userId: string }) {
const router = useRouter()
const handleMouseEnter = () => {
// Preload route (includes loader)
router.preloadRoute({ to: '/users/$userId', params: { userId } })
}
return (
<Link
to="/users/$userId"
params={{ userId }}
onMouseEnter={handleMouseEnter}
>
View User
</Link>
)
}
```
Or use built-in preload:
```typescript
<Link
to="/users/$userId"
params={{ userId: '123' }}
preload="intent" // Preload on hover/focus
>
View User
</Link>
```
## Search Params + Queries
```typescript
// src/routes/users/index.tsx
import { z } from 'zod'
const searchSchema = z.object({
page: z.number().default(1),
filter: z.enum(['active', 'all']).default('all'),
})
export const Route = createFileRoute('/users/')({
validateSearch: searchSchema,
loader: ({ search }) => {
return queryClient.ensureQueryData(
usersListQueryOptions(search.page, search.filter)
)
},
component: UsersPage,
})
function UsersPage() {
const { page, filter } = Route.useSearch()
const { data: users } = useUsersList(page, filter)
return <UserTable users={users} page={page} filter={filter} />
}
```
## Suspense Mode
With Suspense, you don't need separate loading states:
```typescript
export const Route = createFileRoute('/users/$userId')({
loader: ({ params }) =>
queryClient.ensureQueryData(userQueryOptions(params.userId)),
component: UserPage,
})
function UserPage() {
const { userId } = Route.useParams()
// Use Suspense hook - data is NEVER undefined
const { data: user } = useSuspenseQuery(userQueryOptions(userId))
return <div>{user.name}</div>
}
// Wrap route in Suspense boundary (in __root.tsx or layout)
<Suspense fallback={<Spinner />}>
<Outlet />
</Suspense>
```
## Performance Best Practices
1. **Prefetch in Loaders** - Always use loaders to eliminate waterfalls
2. **Use Query Options** - Share configuration between loaders and components
3. **Set Appropriate staleTime** - Tune per query (30s for user data, 10min for static)
4. **Parallel Prefetching** - Use `Promise.all()` for independent queries
5. **Hover Preloading** - Enable `preload="intent"` on critical links
6. **Cache Invalidation** - Be specific with invalidation keys to avoid unnecessary refetches
## DevTools Setup
```typescript
// src/main.tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<ReactQueryDevtools position="bottom-right" />
<TanStackRouterDevtools position="bottom-left" />
</QueryClientProvider>
```
Both auto-hide in production.
## Common Patterns
**List + Detail Pattern:**
```typescript
// List route prefetches list
export const ListRoute = createFileRoute('/users/')({
loader: () => queryClient.ensureQueryData(usersListQueryOptions()),
component: UsersList,
})
// Detail route prefetches specific user
export const DetailRoute = createFileRoute('/users/$userId')({
loader: ({ params }) =>
queryClient.ensureQueryData(userQueryOptions(params.userId)),
component: UserDetail,
})
// Clicking from list to detail uses cached data if available
```
**Edit Form Pattern:**
```typescript
export const EditRoute = createFileRoute('/users/$userId/edit')({
loader: ({ params }) =>
queryClient.ensureQueryData(userQueryOptions(params.userId)),
component: UserEditForm,
})
function UserEditForm() {
const { userId } = Route.useParams()
const { data: user } = useUser(userId)
const updateUser = useUpdateUser()
// Form pre-populated with cached user data
return <Form initialValues={user} onSubmit={updateUser.mutate} />
}
```
## Related Skills
- **tanstack-query** - Comprehensive Query v5 patterns
- **tanstack-router** - Router configuration and usage
- **api-integration** - OpenAPI + Apidog patterns

View File

@@ -0,0 +1,915 @@
---
name: tanstack-query
description: Comprehensive TanStack Query v5 patterns for async state management. Covers breaking changes, query key factories, data transformation, mutations, optimistic updates, authentication, testing with MSW, and anti-patterns. Use for all server state management, data fetching, and cache invalidation tasks.
---
# TanStack Query v5 - Complete Guide
**TanStack Query v5** (October 2023) is the async state manager for this project. It requires React 18+, features first-class Suspense support, improved TypeScript inference, and a 20% smaller bundle. This section covers production-ready patterns based on official documentation and community best practices.
### Breaking Changes in v5
**Key updates you need to know:**
1. **Single Object Signature**: All hooks now accept one configuration object:
```typescript
// ✅ v5 - single object
useQuery({ queryKey, queryFn, ...options })
// ❌ v4 - multiple overloads (deprecated)
useQuery(queryKey, queryFn, options)
```
2. **Renamed Options**:
- `cacheTime` → `gcTime` (garbage collection time)
- `keepPreviousData` → `placeholderData: keepPreviousData`
- `isLoading` now means `isPending && isFetching`
3. **Callbacks Removed from useQuery**:
- `onSuccess`, `onError`, `onSettled` removed from `useQuery`
- Use global QueryCache callbacks instead
- Prevents duplicate executions
4. **Infinite Queries Require initialPageParam**:
- No default value provided
- Must explicitly set `initialPageParam` (e.g., `0` or `null`)
5. **First-Class Suspense**:
- New dedicated hooks: `useSuspenseQuery`, `useSuspenseInfiniteQuery`
- No experimental flag needed
- Data is never undefined at type level
**Migration**: Use the official codemod for automatic migration: `npx @tanstack/query-codemods v5/replace-import-specifier`
### Smart Defaults
Query v5 ships with production-ready defaults:
```typescript
{
staleTime: 0, // Data instantly stale (refetch on mount)
gcTime: 5 * 60_000, // Keep unused cache for 5 minutes
retry: 3, // 3 retries with exponential backoff
refetchOnWindowFocus: true,// Refetch when user returns to tab
refetchOnReconnect: true, // Refetch when network reconnects
}
```
**Philosophy**: React Query is an **async state manager, not a data fetcher**. You provide the Promise; Query manages caching, background updates, and synchronization.
### Client Setup
```typescript
// src/app/providers.tsx
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query'
import { toast } from './toast' // Your notification system
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 0, // Adjust per-query
gcTime: 5 * 60_000, // 5 minutes (v5: formerly cacheTime)
retry: (failureCount, error) => {
// Don't retry on 401 (authentication errors)
if (error?.response?.status === 401) return false
return failureCount < 3
},
},
},
queryCache: new QueryCache({
onError: (error, query) => {
// Only show toast for background errors (when data exists)
if (query.state.data !== undefined) {
toast.error(`Something went wrong: ${error.message}`)
}
},
}),
})
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
```
**DevTools Setup** (auto-excluded in production):
```typescript
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
```
### Architecture: Feature-Based Colocation
**Recommended pattern**: Group queries with related features, not by file type.
```
src/features/
├── Todos/
│ ├── index.tsx # Feature entry point
│ ├── queries.ts # All React Query logic (keys, functions, hooks)
│ ├── types.ts # TypeScript types
│ └── components/ # Feature-specific components
```
**Export only custom hooks** from query files. Keep query functions and keys private:
```typescript
// features/todos/queries.ts
// 1. Query Key Factory (hierarchical structure)
const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
}
// 2. Query Function (private)
const fetchTodos = async (filters: string): Promise<Todo[]> => {
const response = await axios.get('/api/todos', { params: { filters } })
return response.data
}
// 3. Custom Hook (public API)
export const useTodosQuery = (filters: string) => {
return useQuery({
queryKey: todoKeys.list(filters),
queryFn: () => fetchTodos(filters),
staleTime: 30_000, // Fresh for 30 seconds
})
}
```
**Benefits**:
- Prevents key/function mismatches
- Clean public API
- Encapsulation and maintainability
- Easy to locate all query logic for a feature
### Query Key Factories (Essential)
**Structure keys hierarchically** from generic to specific:
```typescript
// ✅ Correct hierarchy
['todos'] // Invalidates everything
['todos', 'list'] // Invalidates all lists
['todos', 'list', { filters }] // Invalidates specific list
['todos', 'detail', 1] // Invalidates specific detail
// ❌ Wrong - flat structure
['todos-list-active'] // Can't partially invalidate
```
**Critical rule**: Query keys must include **ALL variables used in queryFn**. Treat query keys like dependency arrays:
```typescript
// ✅ Correct - includes all variables
const { data } = useQuery({
queryKey: ['todos', filters, sortBy],
queryFn: () => fetchTodos(filters, sortBy),
})
// ❌ Wrong - missing variables
const { data } = useQuery({
queryKey: ['todos'],
queryFn: () => fetchTodos(filters, sortBy), // filters/sortBy not in key!
})
```
**Type consistency matters**: `['todos', '1']` and `['todos', 1]` are **different keys**. Be consistent with types.
### Query Options API (Type Safety)
**The modern pattern** for maximum type safety across your codebase:
```typescript
import { queryOptions } from '@tanstack/react-query'
function todoOptions(id: number) {
return queryOptions({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
staleTime: 5000,
})
}
// ✅ Use everywhere with full type safety
useQuery(todoOptions(1))
queryClient.prefetchQuery(todoOptions(5))
queryClient.setQueryData(todoOptions(42).queryKey, newTodo)
queryClient.getQueryData(todoOptions(42).queryKey) // Fully typed!
```
**Benefits**:
- Single source of truth for query configuration
- Full TypeScript inference for imperatively accessed data
- Reusable across hooks and imperative methods
- Prevents key/function mismatches
### Data Transformation Strategies
Choose the right approach based on your use case:
**1. Transform in queryFn** - Simple cases where cache should store transformed data:
```typescript
const fetchTodos = async (): Promise<Todo[]> => {
const response = await axios.get('/api/todos')
return response.data.map(todo => ({
...todo,
name: todo.name.toUpperCase()
}))
}
```
**2. Transform with `select` option (RECOMMENDED)** - Enables partial subscriptions:
```typescript
// Only re-renders when filtered data changes
export const useTodosQuery = (filters: string) =>
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => data.filter(todo => todo.status === filters),
})
// Only re-renders when count changes
export const useTodosCount = () =>
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => data.length,
})
```
**⚠️ Memoize select functions** to prevent running on every render:
```typescript
// ✅ Stable reference
const transformTodos = (data: Todo[]) => expensiveTransform(data)
const query = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: transformTodos, // Stable function reference
})
// ❌ Runs on every render
const query = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => expensiveTransform(data), // New function every render
})
```
### TypeScript Best Practices
**Let TypeScript infer types** from queryFn rather than specifying generics:
```typescript
// ✅ Recommended - inference
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos, // Returns Promise<Todo[]>
})
// data is Todo[] | undefined
// ❌ Unnecessary - explicit generics
const { data } = useQuery<Todo[]>({
queryKey: ['todos'],
queryFn: fetchTodos,
})
```
**Discriminated unions** automatically narrow types:
```typescript
const { data, isSuccess, isError, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
if (isSuccess) {
// data is Todo[] (never undefined)
}
if (isError) {
// error is defined
}
```
Use `queryOptions` helper for maximum type safety across imperative methods.
### Custom Hooks Pattern
**Always create custom hooks** even for single queries:
```typescript
// ✅ Recommended - custom hook with encapsulation
export function usePost(
id: number,
options?: Omit<UseQueryOptions<Post>, 'queryKey' | 'queryFn'>
) {
return useQuery({
queryKey: ['posts', id],
queryFn: () => getPost(id),
...options,
})
}
// Usage: allows callers to override any option except key/fn
const { data } = usePost(42, { staleTime: 10_000 })
```
**Benefits**:
- Centralizes query logic
- Easy to update all usages
- Consistent configuration
- Better testing
### Error Handling (Multi-Layer Strategy)
**Layer 1: Component-Level** - Specific user feedback:
```typescript
function TodoList() {
const { data, error, isError, isLoading } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
if (isLoading) return <Spinner />
if (isError) return <ErrorAlert>{error.message}</ErrorAlert>
return <ul>{data.map(todo => <TodoItem key={todo.id} {...todo} />)}</ul>
}
```
**Layer 2: Global Error Handling** - Background errors via QueryCache:
```typescript
// Already configured in client setup above
queryCache: new QueryCache({
onError: (error, query) => {
if (query.state.data !== undefined) {
toast.error(`Background error: ${error.message}`)
}
},
})
```
**Layer 3: Error Boundaries** - Catch render errors:
```typescript
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<TodoList />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
```
### Suspense Integration
**First-class Suspense support** in v5 with dedicated hooks:
```typescript
import { useSuspenseQuery } from '@tanstack/react-query'
function TodoList() {
// data is NEVER undefined (type-safe)
const { data } = useSuspenseQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return <ul>{data.map(todo => <TodoItem key={todo.id} {...todo} />)}</ul>
}
// Wrap with Suspense boundary
function App() {
return (
<Suspense fallback={<Spinner />}>
<TodoList />
</Suspense>
)
}
```
**Benefits**:
- Eliminates loading state management
- Data always defined (TypeScript enforced)
- Cleaner component code
- Works with React.lazy for code-splitting
### Mutations with Optimistic Updates
**Basic mutation** with cache invalidation:
```typescript
export function useCreateTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (newTodo: CreateTodoDTO) =>
api.post('/todos', newTodo).then(res => res.data),
onSuccess: (data) => {
// Set detail query immediately
queryClient.setQueryData(['todos', data.id], data)
// Invalidate list queries
queryClient.invalidateQueries({ queryKey: ['todos', 'list'] })
},
})
}
```
**Simple optimistic updates** using `variables`:
```typescript
const addTodoMutation = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/todos', { text: newTodo }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
const { isPending, variables, mutate } = addTodoMutation
return (
<ul>
{todoQuery.data?.map(todo => <li key={todo.id}>{todo.text}</li>)}
{isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
</ul>
)
```
**Advanced optimistic updates** with rollback:
```typescript
useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing queries (prevent race conditions)
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot current data
const previousTodos = queryClient.getQueryData(['todos'])
// Optimistically update cache
queryClient.setQueryData(['todos'], (old: Todo[]) =>
old?.map(todo => todo.id === newTodo.id ? newTodo : todo)
)
// Return context for rollback
return { previousTodos }
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context?.previousTodos)
toast.error('Update failed. Changes reverted.')
},
onSettled: () => {
// Always refetch to ensure consistency
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
```
**Key principles**:
- Cancel ongoing queries in `onMutate` to prevent race conditions
- Snapshot previous data before updating
- Restore snapshot on error
- Always invalidate in `onSettled` for eventual consistency
- **Never mutate cached data directly** - always use immutable updates
### Authentication Integration
**Handle token refresh at HTTP client level** (not React Query):
```typescript
// src/lib/api-client.ts
import axios from 'axios'
import createAuthRefreshInterceptor from 'axios-auth-refresh'
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL,
})
// Add token to requests
apiClient.interceptors.request.use((config) => {
const token = getAccessToken()
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// Refresh token on 401
const refreshAuth = async (failedRequest: any) => {
try {
const newToken = await fetchNewToken()
failedRequest.response.config.headers.Authorization = `Bearer ${newToken}`
setAccessToken(newToken)
return Promise.resolve()
} catch {
removeAccessToken()
window.location.href = '/login'
return Promise.reject()
}
}
createAuthRefreshInterceptor(apiClient, refreshAuth, {
statusCodes: [401],
pauseInstanceWhileRefreshing: true,
})
```
**Protected queries** use the `enabled` option:
```typescript
const useTodos = () => {
const { user } = useUser() // Get current user from auth context
return useQuery({
queryKey: ['todos', user?.id],
queryFn: () => fetchTodos(user.id),
enabled: !!user, // Only execute when user exists
})
}
```
**On logout**: Clear the entire cache with `queryClient.clear()` (not `invalidateQueries()` which triggers refetches):
```typescript
const logout = () => {
removeAccessToken()
queryClient.clear() // Clear all cached data
navigate('/login')
}
```
### Advanced Patterns
**Prefetching** - Eliminate loading states:
```typescript
// Hover prefetching
function ShowDetailsButton() {
const queryClient = useQueryClient()
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['details'],
queryFn: getDetailsData,
staleTime: 60_000, // Consider fresh for 1 minute
})
}
return (
<button onMouseEnter={prefetch} onClick={showDetails}>
Show Details
</button>
)
}
// Route-level prefetching (see Router × Query Integration section)
```
**Infinite Queries** - Infinite scrolling/pagination:
```typescript
function Projects() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
if (isLoading) return <Spinner />
return (
<>
{data.pages.map((page, i) => (
<React.Fragment key={i}>
{page.data.map(project => (
<ProjectCard key={project.id} {...project} />
))}
</React.Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
</>
)
}
```
**Offset-Based Pagination** with `placeholderData`:
```typescript
import { keepPreviousData } from '@tanstack/react-query'
function Posts() {
const [page, setPage] = useState(0)
const { data, isPending, isPlaceholderData } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
placeholderData: keepPreviousData, // Show previous data while fetching
})
return (
<>
{data.posts.map(post => <PostCard key={post.id} {...post} />)}
<button
onClick={() => setPage(p => Math.max(0, p - 1))}
disabled={page === 0}
>
Previous
</button>
<button
onClick={() => setPage(p => p + 1)}
disabled={isPlaceholderData || !data.hasMore}
>
Next
</button>
</>
)
}
```
**Dependent Queries** - Sequential data fetching:
```typescript
function UserProjects({ email }: { email: string }) {
// First query
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: () => getUserByEmail(email),
})
// Second query waits for first
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => getProjectsByUser(user.id),
enabled: !!user?.id, // Only runs when user.id exists
})
return <div>{/* render projects */}</div>
}
```
### Performance Optimization
**staleTime is your primary control** - adjust this, not `gcTime`:
```typescript
// Real-time data (default)
staleTime: 0 // Always considered stale, refetch on mount
// User profiles (changes infrequently)
staleTime: 1000 * 60 * 2 // Fresh for 2 minutes
// Static reference data
staleTime: 1000 * 60 * 10 // Fresh for 10 minutes
```
**Query deduplication** happens automatically - multiple components mounting with identical query keys result in a single network request, but all components receive data.
**Prevent request waterfalls**:
```typescript
// ❌ Waterfall - each query waits for previous
function Dashboard() {
const { data: user } = useQuery(userQuery)
const { data: posts } = useQuery(postsQuery(user?.id))
const { data: stats } = useQuery(statsQuery(user?.id))
}
// ✅ Parallel - all queries start simultaneously
function Dashboard() {
const { data: user } = useQuery(userQuery)
const { data: posts } = useQuery({
...postsQuery(user?.id),
enabled: !!user?.id,
})
const { data: stats } = useQuery({
...statsQuery(user?.id),
enabled: !!user?.id,
})
}
// ✅ Best - prefetch in route loader (see Router × Query Integration)
```
**Never copy server state to local state** - this opts out of background updates:
```typescript
// ❌ Wrong - copies to state, loses reactivity
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
const [todos, setTodos] = useState(data)
// ✅ Correct - use query data directly
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
```
### Testing with Mock Service Worker (MSW)
**MSW is the recommended approach** - mock the network layer:
```typescript
// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/todos', () => {
return HttpResponse.json([
{ id: 1, text: 'Test todo', completed: false },
])
}),
http.post('/api/todos', async ({ request }) => {
const newTodo = await request.json()
return HttpResponse.json({ id: 2, ...newTodo })
}),
]
// src/test/setup.ts
import { setupServer } from 'msw/node'
import { handlers } from './mocks/handlers'
export const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
```
**Create test wrappers** with proper QueryClient:
```typescript
// src/test/utils.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render } from '@testing-library/react'
export function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false, // Prevent retries in tests
gcTime: Infinity,
},
},
})
}
export function renderWithClient(ui: React.ReactElement) {
const testQueryClient = createTestQueryClient()
return render(
<QueryClientProvider client={testQueryClient}>
{ui}
</QueryClientProvider>
)
}
```
**Test queries**:
```typescript
import { renderWithClient } from '@/test/utils'
import { screen } from '@testing-library/react'
test('displays todos', async () => {
renderWithClient(<TodoList />)
// Wait for data to load
expect(await screen.findByText('Test todo')).toBeInTheDocument()
})
test('shows error state', async () => {
// Override handler for this test
server.use(
http.get('/api/todos', () => {
return HttpResponse.json(
{ message: 'Failed to fetch' },
{ status: 500 }
)
})
)
renderWithClient(<TodoList />)
expect(await screen.findByText(/failed/i)).toBeInTheDocument()
})
```
**Critical testing principles**:
- Create new QueryClient per test for isolation
- Set `retry: false` to prevent timeouts
- Use async queries (`findBy*`) for data that loads
- Silence console.error for expected errors
### Anti-Patterns to Avoid
**❌ Don't store query data in Redux/Context**:
- Creates dual sources of truth
- Loses automatic cache invalidation
- Triggers unnecessary renders
**❌ Don't call refetch() with different parameters**:
```typescript
// ❌ Wrong - breaks declarative pattern
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: () => fetchTodos(filters),
})
// Later: refetch with different filters??? Won't work!
// ✅ Correct - include params in key
const [filters, setFilters] = useState('all')
const { data } = useQuery({
queryKey: ['todos', filters],
queryFn: () => fetchTodos(filters),
})
// Changing filters automatically refetches
```
**❌ Don't use queries for local state**:
- Query Cache expects refetchable data
- Use useState/useReducer for client-only state
**❌ Don't create QueryClient inside components**:
```typescript
// ❌ Wrong - new cache every render
function App() {
const client = new QueryClient()
return <QueryClientProvider client={client}>...</QueryClientProvider>
}
// ✅ Correct - stable instance
const queryClient = new QueryClient()
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
```
**❌ Don't ignore loading and error states** - always handle both
**❌ Don't transform data by copying to state** - use `select` option
**❌ Don't mismatch query keys** - be consistent with types (`'1'` vs `1`)
### Cache Timing Guidelines
**staleTime** - How long data is considered fresh:
- `0` (default) - Always stale, refetch on mount/focus
- `30_000` (30s) - Good for user-generated content
- `120_000` (2min) - Good for profile data
- `600_000` (10min) - Good for static reference data
**gcTime** (formerly cacheTime) - How long unused data stays in cache:
- `300_000` (5min, default) - Good for most cases
- `Infinity` - Keep forever (useful with persistence)
- `0` - Immediate garbage collection (not recommended)
**Relationship**: `staleTime` controls refetch frequency, `gcTime` controls memory cleanup.
## Related Skills
- **router-query-integration** - Integrating Query with TanStack Router loaders
- **api-integration** - Apidog + OpenAPI integration
- **react-patterns** - Choose between Query mutations vs React Actions
- **testing-strategy** - Advanced MSW patterns

View File

@@ -0,0 +1,437 @@
---
name: tanstack-router
description: TanStack Router patterns for type-safe, file-based routing. Covers installation, route configuration, typed params/search, layouts, and navigation. Use when setting up routes, implementing navigation, or configuring route loaders.
---
# TanStack Router Patterns
Type-safe, file-based routing for React applications with TanStack Router.
## Installation
```bash
pnpm add @tanstack/react-router
pnpm add -D @tanstack/router-plugin
```
```typescript
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react(),
TanStackRouterVite(), // Generates route tree
],
})
```
## Bootstrap
```typescript
// src/main.tsx
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({ routeTree })
// Register router for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
)
```
## File-Based Routes
```
src/routes/
├── __root.tsx # Root layout (Outlet, providers)
├── index.tsx # "/" route
├── about.tsx # "/about" route
├── users/
│ ├── index.tsx # "/users" route
│ └── $userId.tsx # "/users/:userId" route (dynamic)
└── posts/
├── $postId/
│ ├── index.tsx # "/posts/:postId" route
│ └── edit.tsx # "/posts/:postId/edit" route
└── index.tsx # "/posts" route
```
**Naming Conventions:**
- `__root.tsx` - Root layout (contains `<Outlet />`)
- `index.tsx` - Index route for that path
- `$param.tsx` - Dynamic parameter (e.g., `$userId``:userId`)
- `_layout.tsx` - Layout route (no URL segment)
- `route.lazy.tsx` - Lazy-loaded route
## Root Layout
```typescript
// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users">Users</Link>
</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
<TanStackRouterDevtools /> {/* Auto-hides in production */}
</>
),
})
```
## Basic Route
```typescript
// src/routes/about.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: AboutComponent,
})
function AboutComponent() {
return <div>About Page</div>
}
```
## Dynamic Routes with Params
```typescript
// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$userId')({
component: UserComponent,
})
function UserComponent() {
const { userId } = Route.useParams() // Fully typed!
return <div>User ID: {userId}</div>
}
```
## Typed Search Params
```typescript
// src/routes/users/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const userSearchSchema = z.object({
page: z.number().default(1),
filter: z.enum(['active', 'inactive', 'all']).default('all'),
search: z.string().optional(),
})
export const Route = createFileRoute('/users/')({
validateSearch: userSearchSchema,
component: UsersComponent,
})
function UsersComponent() {
const { page, filter, search } = Route.useSearch() // Fully typed!
return (
<div>
<p>Page: {page}</p>
<p>Filter: {filter}</p>
{search && <p>Search: {search}</p>}
</div>
)
}
```
## Navigation with Link
```typescript
import { Link } from '@tanstack/react-router'
// Basic navigation
<Link to="/about">About</Link>
// With params
<Link to="/users/$userId" params={{ userId: '123' }}>
View User
</Link>
// With search params
<Link
to="/users"
search={{ page: 2, filter: 'active' }}
>
Users Page 2
</Link>
// With state
<Link to="/details" state={{ from: 'home' }}>
Details
</Link>
// Active link styling
<Link
to="/about"
activeProps={{ className: 'text-blue-600 font-bold' }}
inactiveProps={{ className: 'text-gray-600' }}
>
About
</Link>
```
## Programmatic Navigation
```typescript
import { useNavigate } from '@tanstack/react-router'
function MyComponent() {
const navigate = useNavigate()
const handleClick = () => {
// Navigate to route
navigate({ to: '/users' })
// With params
navigate({ to: '/users/$userId', params: { userId: '123' } })
// With search
navigate({ to: '/users', search: { page: 2 } })
// Replace history
navigate({ to: '/login', replace: true })
// Go back
navigate({ to: '..' }) // Relative navigation
}
return <button onClick={handleClick}>Navigate</button>
}
```
## Route Loaders (Data Fetching)
**Basic Loader:**
```typescript
// src/routes/users/$userId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$userId')({
loader: async ({ params }) => {
const user = await fetchUser(params.userId)
return { user }
},
component: UserComponent,
})
function UserComponent() {
const { user } = Route.useLoaderData() // Fully typed!
return <div>{user.name}</div>
}
```
**With TanStack Query Integration** (see **router-query-integration** skill for details):
```typescript
import { queryClient } from '@/app/queryClient'
import { userQuery Options } from '@/features/users/queries'
export const Route = createFileRoute('/users/$userId')({
loader: ({ params }) =>
queryClient.ensureQueryData(userQueryOptions(params.userId)),
component: UserComponent,
})
```
## Layouts
**Layout Route** (`_layout.tsx` - no URL segment):
```typescript
// src/routes/_layout.tsx
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/_layout')({
component: LayoutComponent,
})
function LayoutComponent() {
return (
<div className="dashboard-layout">
<Sidebar />
<div className="content">
<Outlet /> {/* Child routes */}
</div>
</div>
)
}
// Child routes
// src/routes/_layout/dashboard.tsx → "/dashboard"
// src/routes/_layout/settings.tsx → "/settings"
```
## Loading States
```typescript
export const Route = createFileRoute('/users')({
loader: async () => {
const users = await fetchUsers()
return { users }
},
pendingComponent: () => <Spinner />,
errorComponent: ({ error }) => <ErrorMessage>{error.message}</ErrorMessage>,
component: UsersComponent,
})
```
## Error Handling
```typescript
import { ErrorComponent } from '@tanstack/react-router'
export const Route = createFileRoute('/users')({
loader: async () => {
const users = await fetchUsers()
if (!users) throw new Error('Failed to load users')
return { users }
},
errorComponent: ({ error, reset }) => (
<div>
<h1>Error loading users</h1>
<p>{error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
),
component: UsersComponent,
})
```
## Route Context
**Providing Context:**
```typescript
// src/routes/__root.tsx
export const Route = createRootRoute({
beforeLoad: () => ({
user: getCurrentUser(),
}),
component: RootComponent,
})
// Access in child routes
export const Route = createFileRoute('/dashboard')({
component: function Dashboard() {
const { user } = Route.useRouteContext()
return <div>Welcome, {user.name}</div>
},
})
```
## Route Guards / Auth
```typescript
// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context }) => {
if (!context.user) {
throw redirect({ to: '/login' })
}
},
component: Outlet,
})
// Protected routes
// src/routes/_authenticated/dashboard.tsx
// src/routes/_authenticated/profile.tsx
```
## Preloading
**Hover Preload:**
```typescript
<Link
to="/users/$userId"
params={{ userId: '123' }}
preload="intent" // Preload on hover
>
View User
</Link>
```
**Options:**
- `preload="intent"` - Preload on hover/focus
- `preload="render"` - Preload when link renders
- `preload={false}` - No preload (default)
## DevTools
```typescript
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
// Add to root layout
<TanStackRouterDevtools position="bottom-right" />
```
Auto-hides in production builds.
## Best Practices
1. **Use Type-Safe Navigation** - Let TypeScript catch routing errors at compile time
2. **Validate Search Params** - Use Zod schemas for search params
3. **Prefetch Data in Loaders** - Integrate with TanStack Query for optimal data fetching
4. **Use Layouts for Shared UI** - Avoid duplicating layout code across routes
5. **Lazy Load Routes** - Use `route.lazy.tsx` for code splitting
6. **Leverage Route Context** - Share data down the route tree efficiently
## Common Patterns
**Catch-All Route:**
```typescript
// src/routes/$.tsx
export const Route = createFileRoute('/$')({
component: () => <div>404 Not Found</div>,
})
```
**Optional Params:**
```typescript
// Use search params for optional data
const searchSchema = z.object({
optional: z.string().optional(),
})
```
**Multi-Level Dynamic Routes:**
```
/posts/$postId/comments/$commentId
```
## Related Skills
- **tanstack-query** - Data fetching and caching
- **router-query-integration** - Integrating Router loaders with Query
- **core-principles** - Project structure with routes

View File

@@ -0,0 +1,202 @@
---
name: tooling-setup
description: Configure Vite, TypeScript, Biome, and Vitest for React 19 projects. Covers build configuration, strict TypeScript setup, linting/formatting, and testing infrastructure. Use when setting up new projects or updating tool configurations.
---
# Tooling Setup for React 19 Projects
Production-ready configuration for modern frontend tooling with Vite, TypeScript, Biome, and Vitest.
## 1. Vite + React 19 + React Compiler
```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react({
babel: {
// React Compiler must run first:
plugins: ['babel-plugin-react-compiler'],
},
}),
],
})
```
**Verify:** Check DevTools for "Memo ✨" badge on optimized components.
## 2. TypeScript (strict + bundler mode)
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"types": ["vite/client", "vitest"]
},
"include": ["src", "vitest-setup.ts"]
}
```
**Key Settings:**
- `moduleResolution: "bundler"` - Optimized for Vite
- `strict: true` - Enable all strict type checks
- `noUncheckedIndexedAccess: true` - Safer array/object access
- `verbatimModuleSyntax: true` - Explicit import/export
## 3. Biome (formatter + linter)
```bash
npx @biomejs/biome init
npx @biomejs/biome check --write .
```
```json
// biome.json
{
"formatter": { "enabled": true, "lineWidth": 100 },
"linter": {
"enabled": true,
"rules": {
"style": { "noUnusedVariables": "error" }
}
}
}
```
**Usage:**
- `npx biome check .` - Check for issues
- `npx biome check --write .` - Auto-fix issues
- Replaces ESLint + Prettier with one fast tool
## 4. Environment Variables
- Read via `import.meta.env`
- Prefix all app-exposed vars with `VITE_`
- Never place secrets in the client bundle
```typescript
// Access environment variables
const apiUrl = import.meta.env.VITE_API_URL
const isDev = import.meta.env.DEV
const isProd = import.meta.env.PROD
// .env.local (not committed)
VITE_API_URL=https://api.example.com
VITE_ANALYTICS_ID=UA-12345-1
```
## 5. Testing Setup (Vitest)
```typescript
// vitest-setup.ts
import '@testing-library/jest-dom/vitest'
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./vitest-setup.ts'],
coverage: { reporter: ['text', 'html'] }
}
})
```
**Setup Notes:**
- Use React Testing Library for DOM assertions
- Use MSW for API mocks (see **tanstack-query** skill for MSW patterns)
- Add `types: ["vitest", "vitest/jsdom"]` for jsdom globals in tsconfig.json
**Run Tests:**
```bash
npx vitest # Run in watch mode
npx vitest run # Run once
npx vitest --coverage # Generate coverage report
```
## Package Installation
```bash
# Core
pnpm add react@rc react-dom@rc
pnpm add -D vite @vitejs/plugin-react typescript
# Biome (replaces ESLint + Prettier)
pnpm add -D @biomejs/biome
# React Compiler
pnpm add -D babel-plugin-react-compiler
# Testing
pnpm add -D vitest @testing-library/react @testing-library/jest-dom
pnpm add -D @testing-library/user-event jsdom
pnpm add -D msw
# TanStack
pnpm add @tanstack/react-query @tanstack/react-router
pnpm add -D @tanstack/router-plugin @tanstack/react-query-devtools
# Utilities
pnpm add axios zod
```
## Project Scripts
```json
{
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest --coverage",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write ."
}
}
```
## IDE Setup
**VSCode Extensions:**
- Biome (biomejs.biome)
- TypeScript (built-in)
- Vite (antfu.vite)
**VSCode Settings:**
```json
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
```
## Related Skills
- **core-principles** - Project structure and best practices
- **react-patterns** - React 19 specific features
- **testing-strategy** - Advanced testing patterns with MSW

View File

@@ -0,0 +1,441 @@
---
name: ui-implementer
description: Implements UI components from scratch based on design references (Figma, screenshots, mockups) with intelligent validation and adaptive agent switching. Use when user provides a design and wants pixel-perfect UI implementation with design fidelity validation. Triggers automatically when user mentions Figma links, design screenshots, or wants to implement UI from designs.
allowed-tools: Task, AskUserQuestion, Bash, Read, TodoWrite, Glob, Grep
---
# UI Implementer
This Skill implements UI components from scratch based on design references using specialized UI development agents with intelligent validation and adaptive agent switching for optimal results.
## When to use this Skill
Claude should invoke this Skill when:
**Design References Provided:**
- User shares a Figma URL (e.g., "Here's the Figma design: https://figma.com/...")
- User provides a screenshot/mockup path (e.g., "I have a design at /path/to/design.png")
- User mentions a design URL they want to implement
**Intent to Implement UI:**
- "Implement this UI design"
- "Create components from this Figma file"
- "Build this interface from the mockup"
- "Make this screen match the design"
**Pixel-Perfect Requirements:**
- "Make it look exactly like the design"
- "Implement pixel-perfect from Figma"
- "Match the design specifications exactly"
**Examples of User Messages:**
- "Here's a Figma link, can you implement the UserProfile component?"
- "I have a design screenshot, please create the dashboard layout"
- "Implement this navbar from the mockup at designs/navbar.png"
- "Build the product card to match this Figma: https://figma.com/..."
## DO NOT use this Skill when:
- User just wants to validate existing UI (use browser-debugger or /validate-ui instead)
- User wants to fix existing components (use regular developer agent)
- User wants to implement features without design reference (use regular implementation flow)
## Instructions
This Skill implements the same workflow as the `/implement-ui` command. Follow these phases:
### PHASE 0: Initialize Workflow
Create a global todo list to track progress:
```
TodoWrite with:
- PHASE 1: Gather inputs (design reference, component description, preferences)
- PHASE 1: Validate inputs and find target location
- PHASE 2: Launch UI Developer for initial implementation
- PHASE 3: Start validation and iterative fixing loop
- PHASE 3: Quality gate - ensure design fidelity achieved
- PHASE 4: Generate final implementation report
- PHASE 4: Present results and complete handoff
```
### PHASE 1: Gather User Inputs
**Step 1: Extract Design Reference**
Check if user already provided design reference in their message:
- Scan for Figma URLs: `https://figma.com/design/...` or `https://figma.com/file/...`
- Scan for file paths: `/path/to/design.png`, `~/designs/mockup.jpg`
- Scan for remote URLs: `http://example.com/design.png`
If design reference found in user's message:
- Extract and store as `design_reference`
- Log: "Design reference detected: [design_reference]"
If NOT found, ask:
```
I'd like to implement UI from your design reference.
Please provide the design reference:
1. Figma URL (e.g., https://figma.com/design/abc123.../node-id=136-5051)
2. Screenshot file path (local file on your machine)
3. Remote URL (live design reference)
What is your design reference?
```
**Step 2: Extract Component Description**
Check if user mentioned what to implement:
- Look for component names: "UserProfile", "navbar", "dashboard", "ProductCard"
- Look for descriptions: "implement the header", "create the sidebar", "build the form"
If found:
- Extract and store as `component_description`
If NOT found, ask:
```
What UI component(s) should I implement from this design?
Examples:
- "User profile card component"
- "Navigation header with mobile menu"
- "Product listing grid with filters"
- "Dashboard layout with widgets"
What component(s) should I implement?
```
**Step 3: Ask for Target Location**
Ask:
```
Where should I create this component?
Options:
1. Provide a specific directory path (e.g., "src/components/profile/")
2. Let me suggest based on component type
3. I'll tell you after seeing the component structure
Where should I create the component files?
```
Store as `target_location`.
**Step 4: Ask for Application URL**
Ask:
```
What is the URL where I can preview the implementation?
Examples:
- http://localhost:5173 (Vite default)
- http://localhost:3000 (Next.js/CRA default)
- https://staging.yourapp.com
Preview URL?
```
Store as `app_url`.
**Step 5: Ask for UI Developer Codex Preference**
Use AskUserQuestion:
```
Enable intelligent agent switching with UI Developer Codex?
When enabled:
- If UI Developer struggles (2 consecutive failures), switches to UI Developer Codex
- If UI Developer Codex struggles (2 consecutive failures), switches back
- Provides adaptive fixing with both agents for best results
Enable intelligent agent switching?
```
Options:
- "Yes - Enable intelligent agent switching"
- "No - Use only UI Developer"
Store as `codex_enabled` (boolean).
**Step 6: Validate Inputs**
Validate all inputs using the same logic as /implement-ui command:
- Design reference format (Figma/Remote/Local)
- Component description not empty
- Target location valid
- Application URL valid
### PHASE 2: Initial Implementation from Scratch
Launch UI Developer agent using Task tool with `subagent_type: frontend:ui-developer`:
```
Implement the following UI component(s) from scratch based on the design reference.
**Design Reference**: [design_reference]
**Component Description**: [component_description]
**Target Location**: [target_location]
**Application URL**: [app_url]
**Your Task:**
1. **Analyze the design reference:**
- If Figma: Use Figma MCP to fetch design screenshot and specs
- If Remote URL: Use Chrome DevTools MCP to capture screenshot
- If Local file: Read the file to view design
2. **Plan component structure:**
- Determine component hierarchy
- Identify reusable sub-components
- Plan file structure (atomic design principles)
3. **Implement UI components from scratch using modern best practices:**
- React 19 with TypeScript
- Tailwind CSS 4 (utility-first, static classes only, no @apply)
- Mobile-first responsive design
- Accessibility (WCAG 2.1 AA, ARIA attributes)
- Use existing design system components if available
4. **Match design reference exactly:**
- Colors (Tailwind theme or exact hex)
- Typography (families, sizes, weights, line heights)
- Spacing (Tailwind scale: p-4, p-6, etc.)
- Layout (flexbox, grid, alignment)
- Visual elements (borders, shadows, border-radius)
- Interactive states (hover, focus, active, disabled)
5. **Create component files in target location:**
- Use Write tool to create files
- Follow project conventions
- Include TypeScript types
- Add JSDoc comments
6. **Ensure code quality:**
- Run typecheck: `npx tsc --noEmit`
- Run linter: `npm run lint`
- Run build: `npm run build`
- Fix any errors
7. **Provide implementation summary:**
- Files created
- Components implemented
- Key decisions
- Any assumptions
Return detailed implementation summary when complete.
```
Wait for UI Developer to complete.
### PHASE 3: Validation and Adaptive Fixing Loop
Initialize loop variables:
```
iteration_count = 0
max_iterations = 10
previous_issues_count = None
current_issues_count = None
last_agent_used = None
ui_developer_consecutive_failures = 0
codex_consecutive_failures = 0
design_fidelity_achieved = false
```
**Loop: While iteration_count < max_iterations AND NOT design_fidelity_achieved**
**Step 3.1: Launch Designer for Validation**
Use Task tool with `subagent_type: frontend:designer`:
```
Review the implemented UI component against the design reference.
**Iteration**: [iteration_count + 1] / 10
**Design Reference**: [design_reference]
**Component Description**: [component_description]
**Implementation Files**: [List of files]
**Application URL**: [app_url]
**Your Task:**
1. Fetch design reference screenshot
2. Capture implementation screenshot at [app_url]
3. Perform comprehensive design review:
- Colors & theming
- Typography
- Spacing & layout
- Visual elements
- Responsive design
- Accessibility (WCAG 2.1 AA)
- Interactive states
4. Document ALL discrepancies
5. Categorize by severity (CRITICAL/MEDIUM/LOW)
6. Provide actionable fixes with code snippets
7. Calculate design fidelity score (X/60)
8. **Overall assessment:**
- PASS ✅ (score >= 54/60)
- NEEDS IMPROVEMENT ⚠️ (score 40-53/60)
- FAIL ❌ (score < 40/60)
Return detailed design review report.
```
**Step 3.2: Check if Design Fidelity Achieved**
Extract from designer report:
- Overall assessment
- Issue count
- Design fidelity score
If assessment is "PASS":
- Set `design_fidelity_achieved = true`
- Exit loop (success)
**Step 3.3: Determine Fixing Agent (Smart Switching Logic)**
```javascript
function determineFix ingAgent() {
// If Codex not enabled, always use UI Developer
if (!codex_enabled) return "ui-developer"
// Smart switching based on consecutive failures
if (ui_developer_consecutive_failures >= 2) {
// UI Developer struggling - switch to Codex
return "ui-developer-codex"
}
if (codex_consecutive_failures >= 2) {
// Codex struggling - switch to UI Developer
return "ui-developer"
}
// Default: UI Developer (or continue with last successful)
return last_agent_used || "ui-developer"
}
```
**Step 3.4: Launch Fixing Agent**
If `fixing_agent == "ui-developer"`:
- Use Task with `subagent_type: frontend:ui-developer`
- Provide designer feedback
- Request fixes
If `fixing_agent == "ui-developer-codex"`:
- Use Task with `subagent_type: frontend:ui-developer-codex`
- Prepare complete prompt with designer feedback + current code
- Request expert fix plan
**Step 3.5: Update Metrics and Loop**
```javascript
// Check if progress was made
const progress_made = (current_issues_count < previous_issues_count)
if (progress_made) {
// Success! Reset counters
ui_developer_consecutive_failures = 0
codex_consecutive_failures = 0
} else {
// No progress - increment failure counter
if (last_agent_used === "ui-developer") {
ui_developer_consecutive_failures++
} else if (last_agent_used === "ui-developer-codex") {
codex_consecutive_failures++
}
}
// Update for next iteration
previous_issues_count = current_issues_count
iteration_count++
```
Continue loop until design fidelity achieved or max iterations reached.
### PHASE 4: Final Report & Completion
Generate comprehensive implementation report:
```markdown
# UI Implementation Report
## Component Information
- Component: [component_description]
- Design Reference: [design_reference]
- Location: [target_location]
- Preview: [app_url]
## Implementation Summary
- Files Created: [count]
- Components: [list]
## Validation Results
- Iterations: [count] / 10
- Final Status: [PASS/NEEDS IMPROVEMENT/FAIL]
- Design Fidelity Score: [score] / 60
- Issues: [count]
## Agent Performance
- UI Developer: [iterations, successes]
- UI Developer Codex: [iterations, successes] (if enabled)
- Agent Switches: [count] times
## Quality Metrics
- Design Fidelity: [Pass/Needs Improvement]
- Accessibility: [WCAG compliance]
- Responsive: [Mobile/Tablet/Desktop]
- Code Quality: [TypeScript/Lint/Build status]
## How to Use
[Preview instructions]
[Component location]
[Example usage]
## Outstanding Items
[List any remaining issues or recommendations]
```
Present results to user and offer next actions.
## Orchestration Rules
### Smart Agent Switching:
- Track consecutive failures independently for each agent
- Switch after 2 consecutive failures (no progress)
- Reset counters when progress is made
- Log all switches with reasons
- Balance UI Developer (speed) with UI Developer Codex (expertise)
### Loop Prevention:
- Maximum 10 iterations before asking user
- Track progress at each iteration (issue count)
- Ask user for guidance if limit reached
### Quality Gates:
- Design fidelity score >= 54/60 for PASS
- All CRITICAL issues must be resolved
- Accessibility compliance required
## Success Criteria
Complete when:
1. ✅ UI component implemented from scratch
2. ✅ Designer validated against design reference
3. ✅ Design fidelity score >= 54/60
4. ✅ All CRITICAL issues resolved
5. ✅ Accessibility compliant (WCAG 2.1 AA)
6. ✅ Responsive (mobile/tablet/desktop)
7. ✅ Code quality passed (typecheck/lint/build)
8. ✅ Comprehensive report provided
9. ✅ User acknowledges completion
## Notes
- This Skill wraps the `/implement-ui` command workflow
- Use proactively when user provides design references
- Implements from scratch (not for fixing existing UI)
- Smart switching maximizes success rate
- All work on unstaged changes until user approves
- Maximum 10 iterations with user escalation