@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.
@@ -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
+ ```