Initial commit
This commit is contained in:
404
skills/api-integration/SKILL.md
Normal file
404
skills/api-integration/SKILL.md
Normal 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
|
||||
421
skills/api-spec-analyzer/SKILL.md
Normal file
421
skills/api-spec-analyzer/SKILL.md
Normal 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
|
||||
1257
skills/best-practices.md.archive
Normal file
1257
skills/best-practices.md.archive
Normal file
File diff suppressed because it is too large
Load Diff
535
skills/browser-debugger/SKILL.md
Normal file
535
skills/browser-debugger/SKILL.md
Normal 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
|
||||
1298
skills/claudish-usage/SKILL.md
Normal file
1298
skills/claudish-usage/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
123
skills/core-principles/SKILL.md
Normal file
123
skills/core-principles/SKILL.md
Normal 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
|
||||
415
skills/performance-security/SKILL.md
Normal file
415
skills/performance-security/SKILL.md
Normal 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
|
||||
378
skills/react-patterns/SKILL.md
Normal file
378
skills/react-patterns/SKILL.md
Normal 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
|
||||
408
skills/router-query-integration/SKILL.md
Normal file
408
skills/router-query-integration/SKILL.md
Normal 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
|
||||
915
skills/tanstack-query/SKILL.md
Normal file
915
skills/tanstack-query/SKILL.md
Normal 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
|
||||
437
skills/tanstack-router/SKILL.md
Normal file
437
skills/tanstack-router/SKILL.md
Normal 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
|
||||
202
skills/tooling-setup/SKILL.md
Normal file
202
skills/tooling-setup/SKILL.md
Normal 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
|
||||
441
skills/ui-implementer/SKILL.md
Normal file
441
skills/ui-implementer/SKILL.md
Normal 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
|
||||
Reference in New Issue
Block a user