create-tigra 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/create-tigra.js +292 -0
- package/package.json +41 -0
- package/template/.agent/rules/client/01-project-structure.md +326 -0
- package/template/.agent/rules/client/02-component-patterns.md +249 -0
- package/template/.agent/rules/client/03-typescript-rules.md +226 -0
- package/template/.agent/rules/client/04-state-management.md +474 -0
- package/template/.agent/rules/client/05-api-integration.md +129 -0
- package/template/.agent/rules/client/06-forms-validation.md +129 -0
- package/template/.agent/rules/client/07-common-patterns.md +150 -0
- package/template/.agent/rules/client/08-color-system.md +93 -0
- package/template/.agent/rules/client/09-security-rules.md +97 -0
- package/template/.agent/rules/client/10-testing-strategy.md +370 -0
- package/template/.agent/rules/global/ai-edit-safety.md +38 -0
- package/template/.agent/rules/server/01-db-and-migrations.md +242 -0
- package/template/.agent/rules/server/02-general-rules.md +111 -0
- package/template/.agent/rules/server/03-migrations.md +20 -0
- package/template/.agent/rules/server/04-pagination.md +130 -0
- package/template/.agent/rules/server/05-project-conventions.md +71 -0
- package/template/.agent/rules/server/06-response-handling.md +173 -0
- package/template/.agent/rules/server/07-testing-strategy.md +506 -0
- package/template/.agent/rules/server/08-observability.md +180 -0
- package/template/.agent/rules/server/09-api-documentation-v2.md +168 -0
- package/template/.agent/rules/server/10-background-jobs-v2.md +185 -0
- package/template/.agent/rules/server/11-rate-limiting-v2.md +210 -0
- package/template/.agent/rules/server/12-performance-optimization.md +567 -0
- package/template/.claude/rules/client-01-project-structure.md +327 -0
- package/template/.claude/rules/client-02-component-patterns.md +250 -0
- package/template/.claude/rules/client-03-typescript-rules.md +227 -0
- package/template/.claude/rules/client-04-state-management.md +475 -0
- package/template/.claude/rules/client-05-api-integration.md +130 -0
- package/template/.claude/rules/client-06-forms-validation.md +130 -0
- package/template/.claude/rules/client-07-common-patterns.md +151 -0
- package/template/.claude/rules/client-08-color-system.md +94 -0
- package/template/.claude/rules/client-09-security-rules.md +98 -0
- package/template/.claude/rules/client-10-testing-strategy.md +371 -0
- package/template/.claude/rules/global-ai-edit-safety.md +39 -0
- package/template/.claude/rules/server-01-db-and-migrations.md +243 -0
- package/template/.claude/rules/server-02-general-rules.md +112 -0
- package/template/.claude/rules/server-03-migrations.md +21 -0
- package/template/.claude/rules/server-04-pagination.md +131 -0
- package/template/.claude/rules/server-05-project-conventions.md +72 -0
- package/template/.claude/rules/server-06-response-handling.md +174 -0
- package/template/.claude/rules/server-07-testing-strategy.md +507 -0
- package/template/.claude/rules/server-08-observability.md +181 -0
- package/template/.claude/rules/server-09-api-documentation-v2.md +169 -0
- package/template/.claude/rules/server-10-background-jobs-v2.md +186 -0
- package/template/.claude/rules/server-11-rate-limiting-v2.md +211 -0
- package/template/.claude/rules/server-12-performance-optimization.md +568 -0
- package/template/.cursor/rules/client-01-project-structure.mdc +327 -0
- package/template/.cursor/rules/client-02-component-patterns.mdc +250 -0
- package/template/.cursor/rules/client-03-typescript-rules.mdc +227 -0
- package/template/.cursor/rules/client-04-state-management.mdc +475 -0
- package/template/.cursor/rules/client-05-api-integration.mdc +130 -0
- package/template/.cursor/rules/client-06-forms-validation.mdc +130 -0
- package/template/.cursor/rules/client-07-common-patterns.mdc +151 -0
- package/template/.cursor/rules/client-08-color-system.mdc +94 -0
- package/template/.cursor/rules/client-09-security-rules.mdc +98 -0
- package/template/.cursor/rules/client-10-testing-strategy.mdc +371 -0
- package/template/.cursor/rules/global-ai-edit-safety.mdc +39 -0
- package/template/.cursor/rules/server-01-db-and-migrations.mdc +243 -0
- package/template/.cursor/rules/server-02-general-rules.mdc +112 -0
- package/template/.cursor/rules/server-03-migrations.mdc +21 -0
- package/template/.cursor/rules/server-04-pagination.mdc +131 -0
- package/template/.cursor/rules/server-05-project-conventions.mdc +72 -0
- package/template/.cursor/rules/server-06-response-handling.mdc +174 -0
- package/template/.cursor/rules/server-07-testing-strategy.mdc +507 -0
- package/template/.cursor/rules/server-08-observability.mdc +181 -0
- package/template/.cursor/rules/server-09-api-documentation-v2.mdc +169 -0
- package/template/.cursor/rules/server-10-background-jobs-v2.mdc +186 -0
- package/template/.cursor/rules/server-11-rate-limiting-v2.mdc +211 -0
- package/template/.cursor/rules/server-12-performance-optimization.mdc +568 -0
- package/template/CLAUDE.md +207 -0
- package/template/server/.env.example +148 -0
- package/template/server/.tsc-aliasrc.json +12 -0
- package/template/server/README.md +175 -0
- package/template/server/SECURITY.md +190 -0
- package/template/server/biome.json +42 -0
- package/template/server/docker-compose.yml +111 -0
- package/template/server/package.json +83 -0
- package/template/server/postman_collection.json +733 -0
- package/template/server/prisma/schema.prisma +92 -0
- package/template/server/prisma/seed.ts +142 -0
- package/template/server/scripts/wait-for-db.js +60 -0
- package/template/server/src/app.ts +74 -0
- package/template/server/src/config/env.ts +101 -0
- package/template/server/src/hooks/request-timing.hook.ts +26 -0
- package/template/server/src/libs/auth/authenticate.middleware.ts +22 -0
- package/template/server/src/libs/auth/rbac.middleware.test.ts +134 -0
- package/template/server/src/libs/auth/rbac.middleware.ts +147 -0
- package/template/server/src/libs/db.ts +76 -0
- package/template/server/src/libs/error-handler.ts +89 -0
- package/template/server/src/libs/logger.ts +60 -0
- package/template/server/src/libs/queue.ts +79 -0
- package/template/server/src/libs/redis.ts +79 -0
- package/template/server/src/libs/swagger-schemas.ts +16 -0
- package/template/server/src/modules/admin/admin.controller.ts +122 -0
- package/template/server/src/modules/admin/admin.routes.ts +100 -0
- package/template/server/src/modules/admin/admin.schemas.ts +35 -0
- package/template/server/src/modules/admin/admin.service.ts +167 -0
- package/template/server/src/modules/auth/auth.controller.ts +141 -0
- package/template/server/src/modules/auth/auth.integration.test.ts +150 -0
- package/template/server/src/modules/auth/auth.repo.ts +218 -0
- package/template/server/src/modules/auth/auth.routes.ts +204 -0
- package/template/server/src/modules/auth/auth.schemas.ts +137 -0
- package/template/server/src/modules/auth/auth.service.test.ts +119 -0
- package/template/server/src/modules/auth/auth.service.ts +329 -0
- package/template/server/src/modules/auth/auth.types.ts +97 -0
- package/template/server/src/modules/resources/resources.controller.ts +218 -0
- package/template/server/src/modules/resources/resources.repo.ts +253 -0
- package/template/server/src/modules/resources/resources.routes.ts +355 -0
- package/template/server/src/modules/resources/resources.schemas.ts +146 -0
- package/template/server/src/modules/resources/resources.service.ts +218 -0
- package/template/server/src/modules/resources/resources.types.ts +73 -0
- package/template/server/src/plugins/rate-limit.plugin.ts +21 -0
- package/template/server/src/plugins/security.plugin.ts +21 -0
- package/template/server/src/plugins/swagger.plugin.ts +41 -0
- package/template/server/src/routes/health.routes.ts +31 -0
- package/template/server/src/server.ts +142 -0
- package/template/server/src/test/setup.ts +38 -0
- package/template/server/src/types/fastify.d.ts +36 -0
- package/template/server/src/utils/errors.ts +108 -0
- package/template/server/src/utils/pagination.ts +120 -0
- package/template/server/src/utils/response.ts +110 -0
- package/template/server/src/workers/file.worker.ts +106 -0
- package/template/server/tsconfig.build.json +30 -0
- package/template/server/tsconfig.build.tsbuildinfo +1 -0
- package/template/server/tsconfig.json +89 -0
- package/template/server/tsconfig.test.json +22 -0
- package/template/server/vitest.config.ts +98 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: always_on
|
|
3
|
+
globs: "client/**/*"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **SCOPE**: These rules apply specifically to the **client** directory.
|
|
7
|
+
|
|
8
|
+
# TypeScript Rules & Types
|
|
9
|
+
|
|
10
|
+
## TypeScript Configuration
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
// tsconfig.json
|
|
14
|
+
{
|
|
15
|
+
"compilerOptions": {
|
|
16
|
+
"target": "ES2020",
|
|
17
|
+
"useDefineForClassFields": true,
|
|
18
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
19
|
+
"module": "ESNext",
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"moduleResolution": "bundler",
|
|
22
|
+
"allowImportingTsExtensions": true,
|
|
23
|
+
"resolveJsonModule": true,
|
|
24
|
+
"isolatedModules": true,
|
|
25
|
+
"noEmit": true,
|
|
26
|
+
"jsx": "react-jsx",
|
|
27
|
+
"strict": true,
|
|
28
|
+
"noUnusedLocals": true,
|
|
29
|
+
"noUnusedParameters": true,
|
|
30
|
+
"noFallthroughCasesInSwitch": true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Strict Mode Rules
|
|
36
|
+
|
|
37
|
+
- **Always use strict mode**
|
|
38
|
+
- **NO `any` type** (use `unknown` if needed)
|
|
39
|
+
- **Explicit return types** for functions
|
|
40
|
+
- **No implicit any**
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// ❌ BAD
|
|
44
|
+
const fetchResource = async (id) => {
|
|
45
|
+
const response = await api.get(`/resources/${id}`);
|
|
46
|
+
return response.data;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ✅ GOOD
|
|
50
|
+
const fetchResource = async (id: string): Promise<Resource> => {
|
|
51
|
+
const response = await api.get<ApiResponse<Resource>>(`/resources/${id}`);
|
|
52
|
+
return response.data.data;
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Type vs Interface
|
|
57
|
+
|
|
58
|
+
### Use `interface` for:
|
|
59
|
+
- Component props
|
|
60
|
+
- Object shapes
|
|
61
|
+
- Extendable structures
|
|
62
|
+
|
|
63
|
+
### Use `type` for:
|
|
64
|
+
- Unions
|
|
65
|
+
- Intersections
|
|
66
|
+
- Utility types
|
|
67
|
+
- Type aliases
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
// ✅ Interface for props
|
|
71
|
+
interface ResourceCardProps {
|
|
72
|
+
resource: Resource;
|
|
73
|
+
onClick?: () => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ✅ Type for unions
|
|
77
|
+
type UserRole = 'USER' | 'ORG' | 'ADMIN';
|
|
78
|
+
type ResourceStatus = 'active' | 'inactive' | 'deleted';
|
|
79
|
+
|
|
80
|
+
// ✅ Type for intersections
|
|
81
|
+
type ResourceWithOwner = Resource & { owner: User };
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## API Response Types
|
|
85
|
+
|
|
86
|
+
Match backend response structure:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// lib/api/api.types.ts
|
|
90
|
+
|
|
91
|
+
// Base response
|
|
92
|
+
export interface ApiResponse<T> {
|
|
93
|
+
success: boolean;
|
|
94
|
+
message: string;
|
|
95
|
+
data: T;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Paginated response
|
|
99
|
+
export interface PaginatedApiResponse<T> {
|
|
100
|
+
success: boolean;
|
|
101
|
+
message: string;
|
|
102
|
+
data: {
|
|
103
|
+
items: T[];
|
|
104
|
+
pagination: {
|
|
105
|
+
page: number;
|
|
106
|
+
limit: number;
|
|
107
|
+
totalItems: number;
|
|
108
|
+
totalPages: number;
|
|
109
|
+
hasNextPage: boolean;
|
|
110
|
+
hasPreviousPage: boolean;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Error response
|
|
116
|
+
export interface ApiError {
|
|
117
|
+
success: false;
|
|
118
|
+
error: {
|
|
119
|
+
code: string;
|
|
120
|
+
message: string;
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Domain Types
|
|
126
|
+
|
|
127
|
+
Create types matching backend entities:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
// features/resources/types/resource.types.ts
|
|
131
|
+
|
|
132
|
+
export interface Resource {
|
|
133
|
+
id: string;
|
|
134
|
+
ownerId: string;
|
|
135
|
+
title: string;
|
|
136
|
+
summary: string | null;
|
|
137
|
+
price: number;
|
|
138
|
+
status: ResourceStatus;
|
|
139
|
+
createdAt: string;
|
|
140
|
+
updatedAt: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type ResourceStatus = 'active' | 'inactive' | 'deleted';
|
|
144
|
+
|
|
145
|
+
// Request types
|
|
146
|
+
export interface CreateResourceRequest {
|
|
147
|
+
title: string;
|
|
148
|
+
summary?: string;
|
|
149
|
+
price: number;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Function Type Signatures
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// Event handlers
|
|
157
|
+
type ClickHandler = (event: React.MouseEvent<HTMLElement>) => void;
|
|
158
|
+
type ChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
159
|
+
|
|
160
|
+
// Async functions
|
|
161
|
+
type AsyncFunction<T> = () => Promise<T>;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Component Prop Types
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
// Base component props
|
|
168
|
+
interface BaseComponentProps {
|
|
169
|
+
className?: string; // Standard for CSS and Ant Design
|
|
170
|
+
children?: React.ReactNode;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// With generic data
|
|
174
|
+
interface DataComponentProps<T> extends BaseComponentProps {
|
|
175
|
+
data: T;
|
|
176
|
+
onSelect?: (item: T) => void;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Type Guards
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
// Type guard functions
|
|
184
|
+
export const isApiError = (error: unknown): error is ApiError => {
|
|
185
|
+
return (
|
|
186
|
+
typeof error === 'object' &&
|
|
187
|
+
error !== null &&
|
|
188
|
+
'success' in error &&
|
|
189
|
+
error.success === false
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Enum Alternatives (String Unions)
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// ✅ Use string unions + const object
|
|
198
|
+
export const USER_ROLES = {
|
|
199
|
+
USER: 'USER',
|
|
200
|
+
ORG: 'ORGANIZATION',
|
|
201
|
+
ADMIN: 'ADMIN',
|
|
202
|
+
} as const;
|
|
203
|
+
|
|
204
|
+
export type UserRole = (typeof USER_ROLES)[keyof typeof USER_ROLES];
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Discriminated Unions
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
// State machine pattern
|
|
211
|
+
type RequestState<T> =
|
|
212
|
+
| { status: 'idle' }
|
|
213
|
+
| { status: 'loading' }
|
|
214
|
+
| { status: 'success'; data: T }
|
|
215
|
+
| { status: 'error'; error: Error };
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Type Safety Checklist
|
|
219
|
+
|
|
220
|
+
- [ ] Strict mode enabled
|
|
221
|
+
- [ ] No `any` types used
|
|
222
|
+
- [ ] All functions have return types
|
|
223
|
+
- [ ] Props interfaces defined
|
|
224
|
+
- [ ] API response types match backend
|
|
225
|
+
- [ ] Domain types match backend models
|
|
226
|
+
- [ ] Type guards for runtime checks
|
|
227
|
+
- [ ] Discriminated unions for state machines
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: always_on
|
|
3
|
+
globs: "client/**/*"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **SCOPE**: These rules apply specifically to the **client** directory.
|
|
7
|
+
|
|
8
|
+
# State Management Patterns
|
|
9
|
+
|
|
10
|
+
## State Strategy
|
|
11
|
+
|
|
12
|
+
### Decision Matrix
|
|
13
|
+
|
|
14
|
+
| State Type | Tool | Examples |
|
|
15
|
+
|------------|------|----------|
|
|
16
|
+
| **Server Data** | React Query | Resources, profiles, application data from API |
|
|
17
|
+
| **Global Client** | Redux | Auth tokens, current user, theme settings |
|
|
18
|
+
| **Local** | useState | Form inputs, modals, hover state, toggles |
|
|
19
|
+
| **URL** | React Router | Filters, pagination, search query |
|
|
20
|
+
|
|
21
|
+
## React Query (Server State)
|
|
22
|
+
|
|
23
|
+
### Setup
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// app/providers.tsx
|
|
27
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
28
|
+
|
|
29
|
+
const queryClient = new QueryClient({
|
|
30
|
+
defaultOptions: {
|
|
31
|
+
queries: {
|
|
32
|
+
staleTime: 5 * 60 * 1000,
|
|
33
|
+
cacheTime: 10 * 60 * 1000,
|
|
34
|
+
refetchOnWindowFocus: false,
|
|
35
|
+
retry: 1,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Query Key Factory Pattern (MANDATORY)
|
|
44
|
+
|
|
45
|
+
**Why?** Consistent query keys prevent cache bugs and enable powerful invalidation patterns.
|
|
46
|
+
|
|
47
|
+
### Factory Structure
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// features/resources/lib/query-keys.ts
|
|
51
|
+
|
|
52
|
+
export const resourceKeys = {
|
|
53
|
+
// Base key
|
|
54
|
+
all: ['resources'] as const,
|
|
55
|
+
|
|
56
|
+
// List queries
|
|
57
|
+
lists: () => [...resourceKeys.all, 'list'] as const,
|
|
58
|
+
list: (filters: ResourceFilters) => [...resourceKeys.lists(), filters] as const,
|
|
59
|
+
|
|
60
|
+
// Detail queries
|
|
61
|
+
details: () => [...resourceKeys.all, 'detail'] as const,
|
|
62
|
+
detail: (id: string) => [...resourceKeys.details(), id] as const,
|
|
63
|
+
|
|
64
|
+
// User-specific queries
|
|
65
|
+
myResources: () => [...resourceKeys.all, 'my'] as const,
|
|
66
|
+
myResource: (filters: ResourceFilters) => [...resourceKeys.myResources(), filters] as const,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Usage in hooks
|
|
70
|
+
export const useResources = (filters: ResourceFilters) => {
|
|
71
|
+
return useQuery({
|
|
72
|
+
queryKey: resourceKeys.list(filters),
|
|
73
|
+
queryFn: () => resourceService.getResources(filters),
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const useResource = (id: string) => {
|
|
78
|
+
return useQuery({
|
|
79
|
+
queryKey: resourceKeys.detail(id),
|
|
80
|
+
queryFn: () => resourceService.getResource(id),
|
|
81
|
+
enabled: !!id, // Only fetch if ID exists
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Complex Example (Categories with Resources)
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// features/categories/lib/query-keys.ts
|
|
90
|
+
export const categoryKeys = {
|
|
91
|
+
all: ['categories'] as const,
|
|
92
|
+
lists: () => [...categoryKeys.all, 'list'] as const,
|
|
93
|
+
list: (filters: CategoryFilters) => [...categoryKeys.lists(), filters] as const,
|
|
94
|
+
details: () => [...categoryKeys.all, 'detail'] as const,
|
|
95
|
+
detail: (id: string) => [...categoryKeys.details(), id] as const,
|
|
96
|
+
|
|
97
|
+
// Nested resources
|
|
98
|
+
resources: (categoryId: string) => [...categoryKeys.detail(categoryId), 'resources'] as const,
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Invalidation Patterns
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
// After creating a resource
|
|
106
|
+
useMutation({
|
|
107
|
+
mutationFn: resourceService.createResource,
|
|
108
|
+
onSuccess: () => {
|
|
109
|
+
// Invalidate all resource lists
|
|
110
|
+
queryClient.invalidateQueries({ queryKey: resourceKeys.lists() });
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// After updating a specific resource
|
|
115
|
+
useMutation({
|
|
116
|
+
mutationFn: ({ id, data }) => resourceService.updateResource(id, data),
|
|
117
|
+
onSuccess: (_, variables) => {
|
|
118
|
+
// Invalidate this specific resource
|
|
119
|
+
queryClient.invalidateQueries({ queryKey: resourceKeys.detail(variables.id) });
|
|
120
|
+
// Also invalidate lists (resource might move categories)
|
|
121
|
+
queryClient.invalidateQueries({ queryKey: resourceKeys.lists() });
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// After deleting
|
|
126
|
+
useMutation({
|
|
127
|
+
mutationFn: resourceService.deleteResource,
|
|
128
|
+
onSuccess: () => {
|
|
129
|
+
// Remove all resource-related queries
|
|
130
|
+
queryClient.invalidateQueries({ queryKey: resourceKeys.all });
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Query Pattern
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// features/resources/hooks/useResources.ts
|
|
141
|
+
import { useQuery } from '@tanstack/react-query';
|
|
142
|
+
import { resourceService } from '../services/resource.service';
|
|
143
|
+
import { resourceKeys } from '../lib/query-keys';
|
|
144
|
+
|
|
145
|
+
export const useResources = (filters = {}, page = 1, limit = 10) => {
|
|
146
|
+
return useQuery({
|
|
147
|
+
queryKey: resourceKeys.list({ ...filters, page, limit }),
|
|
148
|
+
queryFn: () => resourceService.getResources({ ...filters, page, limit }),
|
|
149
|
+
staleTime: 5 * 60 * 1000,
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Mutation Pattern
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
// features/resources/hooks/useCreateResource.ts
|
|
158
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
159
|
+
import { message } from 'antd'; // Ant Design message
|
|
160
|
+
import { resourceKeys } from '../lib/query-keys';
|
|
161
|
+
|
|
162
|
+
export const useCreateResource = () => {
|
|
163
|
+
const queryClient = useQueryClient();
|
|
164
|
+
|
|
165
|
+
return useMutation({
|
|
166
|
+
mutationFn: resourceService.createResource,
|
|
167
|
+
onSuccess: () => {
|
|
168
|
+
queryClient.invalidateQueries({ queryKey: resourceKeys.lists() });
|
|
169
|
+
message.success('Resource created!');
|
|
170
|
+
},
|
|
171
|
+
onError: (error) => {
|
|
172
|
+
message.error(getErrorMessage(error));
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Optimistic Updates
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
export const useToggleStatus = () => {
|
|
182
|
+
const queryClient = useQueryClient();
|
|
183
|
+
|
|
184
|
+
return useMutation({
|
|
185
|
+
mutationFn: ({ id, status }) => resourceService.updateStatus(id, status),
|
|
186
|
+
onMutate: async ({ id, status }) => {
|
|
187
|
+
// Cancel outgoing queries
|
|
188
|
+
await queryClient.cancelQueries({ queryKey: resourceKeys.detail(id) });
|
|
189
|
+
|
|
190
|
+
// Snapshot previous value
|
|
191
|
+
const previous = queryClient.getQueryData(resourceKeys.detail(id));
|
|
192
|
+
|
|
193
|
+
// Optimistically update
|
|
194
|
+
queryClient.setQueryData(resourceKeys.detail(id), (old: any) => ({
|
|
195
|
+
...old,
|
|
196
|
+
status
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
return { previous, id };
|
|
200
|
+
},
|
|
201
|
+
onError: (err, variables, context) => {
|
|
202
|
+
// Rollback on error
|
|
203
|
+
if (context?.previous) {
|
|
204
|
+
queryClient.setQueryData(resourceKeys.detail(context.id), context.previous);
|
|
205
|
+
}
|
|
206
|
+
message.error('Failed to update status');
|
|
207
|
+
},
|
|
208
|
+
onSettled: (data, error, variables) => {
|
|
209
|
+
// Refetch to ensure sync
|
|
210
|
+
queryClient.invalidateQueries({ queryKey: resourceKeys.detail(variables.id) });
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Redux (Global Client State)
|
|
219
|
+
|
|
220
|
+
### When to Use Redux
|
|
221
|
+
- Authentication state (user, tokens)
|
|
222
|
+
- Global UI state (theme, sidebar collapsed)
|
|
223
|
+
- App-wide settings
|
|
224
|
+
|
|
225
|
+
### Slice Pattern
|
|
226
|
+
Standard Redux Toolkit pattern for auth and global UI state.
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
// features/auth/store/authSlice.ts
|
|
230
|
+
import { createSlice } from '@reduxjs/toolkit';
|
|
231
|
+
|
|
232
|
+
interface AuthState {
|
|
233
|
+
user: User | null;
|
|
234
|
+
tokens: { accessToken: string; refreshToken: string } | null;
|
|
235
|
+
isAuthenticated: boolean;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const initialState: AuthState = {
|
|
239
|
+
user: null,
|
|
240
|
+
tokens: null,
|
|
241
|
+
isAuthenticated: false,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const authSlice = createSlice({
|
|
245
|
+
name: 'auth',
|
|
246
|
+
initialState,
|
|
247
|
+
reducers: {
|
|
248
|
+
setCredentials: (state, action) => {
|
|
249
|
+
state.user = action.payload.user;
|
|
250
|
+
state.tokens = action.payload.tokens;
|
|
251
|
+
state.isAuthenticated = true;
|
|
252
|
+
},
|
|
253
|
+
logout: (state) => {
|
|
254
|
+
state.user = null;
|
|
255
|
+
state.tokens = null;
|
|
256
|
+
state.isAuthenticated = false;
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
export const { setCredentials, logout } = authSlice.actions;
|
|
262
|
+
export default authSlice.reducer;
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Local State
|
|
268
|
+
Use `useState` for UI-only state (e.g., `isModalOpen`, form inputs before submission).
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
export const ResourceCard = ({ resource }) => {
|
|
272
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
273
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div
|
|
277
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
278
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
279
|
+
>
|
|
280
|
+
{/* UI */}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## URL State
|
|
289
|
+
Use `useSearchParams` from `react-router-dom` for filters and pagination to ensure page refreshes and direct links work as expected.
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
import { useSearchParams } from 'react-router-dom';
|
|
293
|
+
|
|
294
|
+
export const ResourcesPage = () => {
|
|
295
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
296
|
+
|
|
297
|
+
const page = parseInt(searchParams.get('page') || '1');
|
|
298
|
+
const category = searchParams.get('category') || '';
|
|
299
|
+
|
|
300
|
+
const handleFilterChange = (newCategory: string) => {
|
|
301
|
+
setSearchParams({ page: '1', category: newCategory });
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const { data } = useResources({ category }, page);
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div>
|
|
308
|
+
<CategoryFilter value={category} onChange={handleFilterChange} />
|
|
309
|
+
<ResourceList resources={data?.items} />
|
|
310
|
+
<Pagination
|
|
311
|
+
current={page}
|
|
312
|
+
total={data?.pagination.totalPages}
|
|
313
|
+
onChange={(newPage) => setSearchParams({ page: String(newPage), category })}
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Advanced Query Patterns
|
|
323
|
+
|
|
324
|
+
### Dependent Queries
|
|
325
|
+
```tsx
|
|
326
|
+
export const useResourceWithOwner = (resourceId: string) => {
|
|
327
|
+
// First fetch resource
|
|
328
|
+
const { data: resource } = useQuery({
|
|
329
|
+
queryKey: resourceKeys.detail(resourceId),
|
|
330
|
+
queryFn: () => resourceService.getResource(resourceId),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Then fetch owner (depends on resource)
|
|
334
|
+
const { data: owner } = useQuery({
|
|
335
|
+
queryKey: ['users', resource?.ownerId],
|
|
336
|
+
queryFn: () => userService.getUser(resource!.ownerId),
|
|
337
|
+
enabled: !!resource?.ownerId, // Only run when we have ownerId
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return { resource, owner };
|
|
341
|
+
};
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Parallel Queries
|
|
345
|
+
```tsx
|
|
346
|
+
export const useDashboardData = () => {
|
|
347
|
+
const resources = useQuery({
|
|
348
|
+
queryKey: resourceKeys.lists(),
|
|
349
|
+
queryFn: resourceService.getResources,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const categories = useQuery({
|
|
353
|
+
queryKey: categoryKeys.lists(),
|
|
354
|
+
queryFn: categoryService.getCategories,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const stats = useQuery({
|
|
358
|
+
queryKey: ['stats'],
|
|
359
|
+
queryFn: statsService.getStats,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
isLoading: resources.isLoading || categories.isLoading || stats.isLoading,
|
|
364
|
+
data: {
|
|
365
|
+
resources: resources.data,
|
|
366
|
+
categories: categories.data,
|
|
367
|
+
stats: stats.data,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Infinite Queries (Load More)
|
|
374
|
+
```tsx
|
|
375
|
+
export const useInfiniteResources = (filters: ResourceFilters) => {
|
|
376
|
+
return useInfiniteQuery({
|
|
377
|
+
queryKey: [...resourceKeys.lists(), 'infinite', filters],
|
|
378
|
+
queryFn: ({ pageParam = 1 }) =>
|
|
379
|
+
resourceService.getResources({ ...filters, page: pageParam }),
|
|
380
|
+
getNextPageParam: (lastPage) =>
|
|
381
|
+
lastPage.pagination.hasNextPage ? lastPage.pagination.page + 1 : undefined,
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Usage in component
|
|
386
|
+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteResources(filters);
|
|
387
|
+
|
|
388
|
+
<Button onClick={() => fetchNextPage()} disabled={!hasNextPage} loading={isFetchingNextPage}>
|
|
389
|
+
Load More
|
|
390
|
+
</Button>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Anti-Patterns
|
|
396
|
+
|
|
397
|
+
### ❌ DO NOT:
|
|
398
|
+
```tsx
|
|
399
|
+
// Don't store server data in Redux
|
|
400
|
+
const dispatch = useDispatch();
|
|
401
|
+
const resources = await resourceService.getResources();
|
|
402
|
+
dispatch(setResources(resources)); // BAD
|
|
403
|
+
|
|
404
|
+
// Don't use string literals for query keys
|
|
405
|
+
useQuery(['resources'], () => ...); // BAD - inconsistent
|
|
406
|
+
|
|
407
|
+
// Don't forget to invalidate after mutations
|
|
408
|
+
useMutation({
|
|
409
|
+
mutationFn: createResource,
|
|
410
|
+
// Missing onSuccess invalidation - cache will be stale!
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### ✅ DO:
|
|
415
|
+
```tsx
|
|
416
|
+
// Use React Query for server data
|
|
417
|
+
const { data: resources } = useQuery({
|
|
418
|
+
queryKey: resourceKeys.lists(),
|
|
419
|
+
queryFn: resourceService.getResources,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Always use query key factories
|
|
423
|
+
queryClient.invalidateQueries({ queryKey: resourceKeys.lists() });
|
|
424
|
+
|
|
425
|
+
// Always invalidate after mutations
|
|
426
|
+
useMutation({
|
|
427
|
+
mutationFn: createResource,
|
|
428
|
+
onSuccess: () => {
|
|
429
|
+
queryClient.invalidateQueries({ queryKey: resourceKeys.lists() });
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Prefetching Data
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
export const ResourcesPage = () => {
|
|
440
|
+
const queryClient = useQueryClient();
|
|
441
|
+
|
|
442
|
+
// Prefetch next page on hover
|
|
443
|
+
const handleResourceHover = (id: string) => {
|
|
444
|
+
queryClient.prefetchQuery({
|
|
445
|
+
queryKey: resourceKeys.detail(id),
|
|
446
|
+
queryFn: () => resourceService.getResource(id),
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div>
|
|
452
|
+
{resources.map((resource) => (
|
|
453
|
+
<ResourceCard
|
|
454
|
+
key={resource.id}
|
|
455
|
+
resource={resource}
|
|
456
|
+
onMouseEnter={() => handleResourceHover(resource.id)}
|
|
457
|
+
/>
|
|
458
|
+
))}
|
|
459
|
+
</div>
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Checklist
|
|
467
|
+
|
|
468
|
+
- [ ] Query key factories created for all domains
|
|
469
|
+
- [ ] React Query for all server data
|
|
470
|
+
- [ ] Redux ONLY for auth and global UI state
|
|
471
|
+
- [ ] URL state for filters/pagination
|
|
472
|
+
- [ ] Proper invalidation after mutations
|
|
473
|
+
- [ ] Optimistic updates for instant feedback
|
|
474
|
+
- [ ] Error handling in mutations
|
|
475
|
+
- [ ] Loading states handled in UI
|