@famgia/omnify-laravel 0.0.119 → 0.0.120
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/dist/{chunk-7I6UNXOD.js → chunk-NMX3TLZT.js} +8 -1
- package/dist/{chunk-7I6UNXOD.js.map → chunk-NMX3TLZT.js.map} +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +7 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.js +1 -1
- package/package.json +4 -4
- package/stubs/ai-guides/claude-checklists/react.md.stub +108 -0
- package/stubs/ai-guides/cursor/omnify-schema.mdc.stub +339 -0
- package/stubs/ai-guides/cursor/react-design.mdc.stub +693 -0
- package/stubs/ai-guides/cursor/react-form.mdc.stub +277 -0
- package/stubs/ai-guides/cursor/react-services.mdc.stub +304 -0
- package/stubs/ai-guides/cursor/react.mdc.stub +336 -0
- package/stubs/ai-guides/cursor/schema-create.mdc.stub +344 -0
- package/stubs/ai-guides/react/README.md.stub +221 -0
- package/stubs/ai-guides/react/antd-guide.md.stub +457 -0
- package/stubs/ai-guides/react/checklist.md.stub +108 -0
- package/stubs/ai-guides/react/datetime-guide.md.stub +137 -0
- package/stubs/ai-guides/react/design-philosophy.md.stub +363 -0
- package/stubs/ai-guides/react/i18n-guide.md.stub +211 -0
- package/stubs/ai-guides/react/laravel-integration.md.stub +181 -0
- package/stubs/ai-guides/react/service-pattern.md.stub +180 -0
- package/stubs/ai-guides/react/tanstack-query.md.stub +339 -0
- package/stubs/ai-guides/react/types-guide.md.stub +671 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# TanStack Query Guide
|
|
2
|
+
|
|
3
|
+
> **Related:** [README](./README.md) | [Service Pattern](./service-pattern.md)
|
|
4
|
+
|
|
5
|
+
## Query Keys Pattern
|
|
6
|
+
|
|
7
|
+
### Structure
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// lib/queryKeys.ts
|
|
11
|
+
|
|
12
|
+
import type { UserListParams } from "@/services/users"; // Import from service
|
|
13
|
+
import type { PostListParams } from "@/services/posts";
|
|
14
|
+
|
|
15
|
+
export const queryKeys = {
|
|
16
|
+
// Simple key
|
|
17
|
+
user: ["user"] as const,
|
|
18
|
+
|
|
19
|
+
// Resource with nested keys - USE TYPED PARAMS
|
|
20
|
+
users: {
|
|
21
|
+
all: ["users"] as const,
|
|
22
|
+
lists: () => [...queryKeys.users.all, "list"] as const,
|
|
23
|
+
list: (params?: UserListParams) => [...queryKeys.users.lists(), params] as const,
|
|
24
|
+
details: () => [...queryKeys.users.all, "detail"] as const,
|
|
25
|
+
detail: (id: number) => [...queryKeys.users.details(), id] as const,
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
posts: {
|
|
29
|
+
all: ["posts"] as const,
|
|
30
|
+
lists: () => [...queryKeys.posts.all, "list"] as const,
|
|
31
|
+
list: (params?: PostListParams) => [...queryKeys.posts.lists(), params] as const,
|
|
32
|
+
details: () => [...queryKeys.posts.all, "detail"] as const,
|
|
33
|
+
detail: (id: number) => [...queryKeys.posts.details(), id] as const,
|
|
34
|
+
byUser: (userId: number) => [...queryKeys.posts.all, "user", userId] as const,
|
|
35
|
+
},
|
|
36
|
+
} as const;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### ⚠️ Type Rule
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// ✅ DO: Import and use specific types from service
|
|
43
|
+
import type { UserListParams } from "@/services/users";
|
|
44
|
+
list: (params?: UserListParams) => [...]
|
|
45
|
+
|
|
46
|
+
// ❌ DON'T: Use generic Record type
|
|
47
|
+
list: (params?: Record<string, unknown>) => [...] // Hard to read, no autocomplete
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Rules
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// ✅ DO: Use query key factory
|
|
54
|
+
useQuery({
|
|
55
|
+
queryKey: queryKeys.users.detail(id),
|
|
56
|
+
queryFn: () => userService.get(id),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ❌ DON'T: Hardcode query keys
|
|
60
|
+
useQuery({
|
|
61
|
+
queryKey: ["users", "detail", id], // Hard to maintain
|
|
62
|
+
queryFn: () => userService.get(id),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ✅ DO: Include all dependencies in key
|
|
66
|
+
useQuery({
|
|
67
|
+
queryKey: queryKeys.users.list({ page, search }), // Refetches when params change
|
|
68
|
+
queryFn: () => userService.list({ page, search }),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ❌ DON'T: Omit dependencies
|
|
72
|
+
useQuery({
|
|
73
|
+
queryKey: queryKeys.users.list(), // Won't refetch when params change
|
|
74
|
+
queryFn: () => userService.list({ page, search }),
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Mutation Pattern
|
|
81
|
+
|
|
82
|
+
### Standard CRUD Mutations
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
"use client";
|
|
86
|
+
|
|
87
|
+
import { Form, Button } from "antd";
|
|
88
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
89
|
+
import { message } from "antd";
|
|
90
|
+
import { useRouter } from "next/navigation";
|
|
91
|
+
import { useTranslations } from "next-intl";
|
|
92
|
+
import { queryKeys } from "@/lib/queryKeys";
|
|
93
|
+
import { getFormErrors } from "@/lib/api";
|
|
94
|
+
import { userService, UserUpdateInput } from "@/services/users";
|
|
95
|
+
|
|
96
|
+
export default function CreateUserPage() {
|
|
97
|
+
const t = useTranslations(); // No namespace = access all
|
|
98
|
+
const router = useRouter();
|
|
99
|
+
const queryClient = useQueryClient();
|
|
100
|
+
const [form] = Form.useForm();
|
|
101
|
+
|
|
102
|
+
// CREATE mutation
|
|
103
|
+
const createMutation = useMutation({
|
|
104
|
+
mutationFn: userService.create,
|
|
105
|
+
onSuccess: () => {
|
|
106
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
107
|
+
message.success(t("messages.created"));
|
|
108
|
+
router.push("/users");
|
|
109
|
+
},
|
|
110
|
+
onError: (error) => {
|
|
111
|
+
form.setFields(getFormErrors(error));
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// UPDATE mutation
|
|
116
|
+
const updateMutation = useMutation({
|
|
117
|
+
mutationFn: ({ id, data }: { id: number; data: UserUpdateInput }) =>
|
|
118
|
+
userService.update(id, data),
|
|
119
|
+
onSuccess: (_, { id }) => {
|
|
120
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
121
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
|
|
122
|
+
message.success(t("messages.updated"));
|
|
123
|
+
router.push(`/users/${id}`);
|
|
124
|
+
},
|
|
125
|
+
onError: (error) => {
|
|
126
|
+
form.setFields(getFormErrors(error));
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// DELETE mutation
|
|
131
|
+
const deleteMutation = useMutation({
|
|
132
|
+
mutationFn: userService.delete,
|
|
133
|
+
onSuccess: () => {
|
|
134
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
135
|
+
message.success(t("messages.deleted"));
|
|
136
|
+
router.push("/users");
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Form form={form} onFinish={(values) => createMutation.mutate(values)}>
|
|
142
|
+
{/* ... form fields ... */}
|
|
143
|
+
<Button type="primary" htmlType="submit" loading={createMutation.isPending}>
|
|
144
|
+
{t("common.save")}
|
|
145
|
+
</Button>
|
|
146
|
+
</Form>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Mutation Rules
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// ✅ DO: Always invalidate related queries after mutation
|
|
155
|
+
onSuccess: () => {
|
|
156
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ❌ DON'T: Forget to invalidate
|
|
160
|
+
onSuccess: () => {
|
|
161
|
+
message.success("Saved!"); // Data won't refresh!
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ✅ DO: Handle form errors from Laravel
|
|
165
|
+
onError: (error) => {
|
|
166
|
+
form.setFields(getFormErrors(error));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ❌ DON'T: Ignore errors
|
|
170
|
+
onError: (error) => {
|
|
171
|
+
console.log(error); // User sees nothing
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ✅ DO: Show loading state
|
|
175
|
+
<Button loading={mutation.isPending}>Submit</Button>
|
|
176
|
+
|
|
177
|
+
// ❌ DON'T: No loading feedback
|
|
178
|
+
<Button>Submit</Button> // User can double-click
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Advanced Tips
|
|
184
|
+
|
|
185
|
+
### Keep Queries Simple
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// ✅ SIMPLE: queryFn just calls service
|
|
189
|
+
useQuery({
|
|
190
|
+
queryKey: queryKeys.users.list(filters),
|
|
191
|
+
queryFn: () => userService.list(filters),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ❌ OVER-ENGINEERED: Logic in queryFn
|
|
195
|
+
useQuery({
|
|
196
|
+
queryKey: queryKeys.users.list(filters),
|
|
197
|
+
queryFn: async () => {
|
|
198
|
+
const data = await userService.list(filters);
|
|
199
|
+
return data.map(transform).filter(validate); // Move this to service!
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Query Key = All Dependencies
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// ✅ CORRECT: Key includes all params → auto refetch when changed
|
|
208
|
+
useQuery({
|
|
209
|
+
queryKey: queryKeys.users.list({ page, search, status }),
|
|
210
|
+
queryFn: () => userService.list({ page, search, status }),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ❌ WRONG: Missing deps in key → stale data
|
|
214
|
+
useQuery({
|
|
215
|
+
queryKey: queryKeys.users.list(),
|
|
216
|
+
queryFn: () => userService.list({ page, search }), // Params not in key!
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Conditional Queries with `enabled`
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Fetch user first, then fetch user's posts
|
|
224
|
+
const { data: user } = useQuery({
|
|
225
|
+
queryKey: queryKeys.user,
|
|
226
|
+
queryFn: authService.me,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const { data: posts } = useQuery({
|
|
230
|
+
queryKey: queryKeys.posts.byUser(user?.id!),
|
|
231
|
+
queryFn: () => postService.listByUser(user!.id),
|
|
232
|
+
enabled: !!user, // ← Only runs when user exists
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Invalidate Correctly
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// ✅ Invalidate by prefix (all user queries)
|
|
240
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
241
|
+
|
|
242
|
+
// ✅ Invalidate specific query
|
|
243
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
|
|
244
|
+
|
|
245
|
+
// ❌ DON'T invalidate everything
|
|
246
|
+
queryClient.invalidateQueries(); // Too broad!
|
|
247
|
+
|
|
248
|
+
// ❌ DON'T refetch manually
|
|
249
|
+
await userService.create(data);
|
|
250
|
+
refetch(); // Wrong! Use invalidateQueries
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Optimistic Updates (Use Sparingly)
|
|
254
|
+
|
|
255
|
+
Only for instant feedback UX (like/unlike, toggle, drag-drop):
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const likeMutation = useMutation({
|
|
259
|
+
mutationFn: postService.like,
|
|
260
|
+
onMutate: async (postId) => {
|
|
261
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.posts.detail(postId) });
|
|
262
|
+
const previous = queryClient.getQueryData(queryKeys.posts.detail(postId));
|
|
263
|
+
queryClient.setQueryData(queryKeys.posts.detail(postId), (old: Post) => ({
|
|
264
|
+
...old,
|
|
265
|
+
liked: true,
|
|
266
|
+
likesCount: old.likesCount + 1,
|
|
267
|
+
}));
|
|
268
|
+
return { previous };
|
|
269
|
+
},
|
|
270
|
+
onError: (err, postId, context) => {
|
|
271
|
+
queryClient.setQueryData(queryKeys.posts.detail(postId), context?.previous);
|
|
272
|
+
},
|
|
273
|
+
onSettled: (data, error, postId) => {
|
|
274
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.posts.detail(postId) });
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Prefetching (For Better UX)
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
<Link
|
|
283
|
+
href={`/users/${user.id}`}
|
|
284
|
+
onMouseEnter={() => {
|
|
285
|
+
queryClient.prefetchQuery({
|
|
286
|
+
queryKey: queryKeys.users.detail(user.id),
|
|
287
|
+
queryFn: () => userService.get(user.id),
|
|
288
|
+
});
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
{user.name}
|
|
292
|
+
</Link>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Common Mistakes
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// ❌ Mixing server state with local state
|
|
301
|
+
const [users, setUsers] = useState([]); // DELETE THIS
|
|
302
|
+
const { data } = useQuery({...}); // USE THIS ONLY
|
|
303
|
+
|
|
304
|
+
// ❌ Fetching in useEffect
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
fetchUsers().then(setUsers); // WRONG
|
|
307
|
+
}, []);
|
|
308
|
+
// ✅ Use useQuery instead
|
|
309
|
+
|
|
310
|
+
// ❌ Missing error handling in mutation
|
|
311
|
+
const mutation = useMutation({
|
|
312
|
+
mutationFn: userService.create,
|
|
313
|
+
onSuccess: () => message.success("Created"),
|
|
314
|
+
// MISSING: onError for form validation!
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ✅ Always handle errors
|
|
318
|
+
const mutation = useMutation({
|
|
319
|
+
mutationFn: userService.create,
|
|
320
|
+
onSuccess: () => message.success(t("created")),
|
|
321
|
+
onError: (error) => form.setFields(getFormErrors(error)),
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## When NOT to Use TanStack Query
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// ❌ For client-only state (use useState or Zustand)
|
|
331
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
332
|
+
const [selectedItems, setSelectedItems] = useState<number[]>([]);
|
|
333
|
+
|
|
334
|
+
// ❌ For derived/computed values (use useMemo)
|
|
335
|
+
const filteredUsers = useMemo(
|
|
336
|
+
() => users.filter(u => u.active),
|
|
337
|
+
[users]
|
|
338
|
+
);
|
|
339
|
+
```
|