ghcopilot-hub 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/README.md +176 -0
- package/hub/agents/README.md +243 -0
- package/hub/agents/archiver.agent.md +231 -0
- package/hub/agents/explore.agent.md +49 -0
- package/hub/agents/implementador.agent.md +176 -0
- package/hub/agents/librarian.agent.md +34 -0
- package/hub/agents/momus.agent.md +130 -0
- package/hub/agents/oracle.agent.md +52 -0
- package/hub/agents/plan-guardian.agent.md +109 -0
- package/hub/agents/planificador.agent.md +295 -0
- package/hub/agents/test-sentinel.agent.md +106 -0
- package/hub/base/.github/copilot-instructions.md +10 -0
- package/hub/base/.github/instructions/ghcopilot-hub.instructions.md +6 -0
- package/hub/base/.github/prompts/ghcopilot-hub-maintenance.prompt.md +8 -0
- package/hub/base/.vscode/settings.json +1 -0
- package/hub/packs/base-web.json +4 -0
- package/hub/packs/nextjs-ssr.json +4 -0
- package/hub/packs/node-api.json +4 -0
- package/hub/packs/spa-tanstack.json +4 -0
- package/hub/skills/architecture-testing/SKILL.md +108 -0
- package/hub/skills/architecture-testing/references/archunitts.md +46 -0
- package/hub/skills/ghcopilot-hub-consumer/SKILL.md +115 -0
- package/hub/skills/ghcopilot-hub-consumer/references/workflow.md +39 -0
- package/hub/skills/mermaid-expert/SKILL.md +152 -0
- package/hub/skills/mermaid-expert/assets/examples/c4_model.md +121 -0
- package/hub/skills/mermaid-expert/assets/examples/flowchart.md +123 -0
- package/hub/skills/mermaid-expert/assets/examples/img/base_minimal.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/corporate.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/dark.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/dark_neo.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/default_neo.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/forest_corp.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/handdrawn.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/neo.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/neutral_sketch.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/img/retro.png +0 -0
- package/hub/skills/mermaid-expert/assets/examples/sequence.md +116 -0
- package/hub/skills/mermaid-expert/assets/examples/styles_and_looks.md +102 -0
- package/hub/skills/mermaid-expert/assets/examples/technical.md +130 -0
- package/hub/skills/mermaid-expert/assets/examples.md +57 -0
- package/hub/skills/mermaid-expert/references/cheatsheet.md +88 -0
- package/hub/skills/mermaid-expert/references/validation.md +66 -0
- package/hub/skills/react/SKILL.md +235 -0
- package/hub/skills/react/references/common-mistakes.md +518 -0
- package/hub/skills/react/references/composition-patterns.md +526 -0
- package/hub/skills/react/references/effects-patterns.md +396 -0
- package/hub/skills/react/references/react-compiler.md +268 -0
- package/hub/skills/react-hook-form/SKILL.md +291 -0
- package/hub/skills/react-hook-form/references/field-arrays.md +98 -0
- package/hub/skills/react-hook-form/references/integration.md +102 -0
- package/hub/skills/react-hook-form/references/performance.md +96 -0
- package/hub/skills/skill-creator/SKILL.md +152 -0
- package/hub/skills/skill-creator/assets/SKILL-TEMPLATE.md +84 -0
- package/hub/skills/skill-judge/README.md +261 -0
- package/hub/skills/skill-judge/SKILL.md +806 -0
- package/hub/skills/tailwind/SKILL.md +200 -0
- package/hub/skills/tanstack/SKILL.md +284 -0
- package/hub/skills/tanstack/references/loader-adapter-examples.md +79 -0
- package/hub/skills/tanstack/references/query-options-examples.md +115 -0
- package/hub/skills/tanstack/references/resilience-patterns.md +110 -0
- package/hub/skills/tanstack/references/suspense-consumption-examples.md +82 -0
- package/hub/skills/tanstack-query/SKILL.md +241 -0
- package/hub/skills/tanstack-query/references/advanced-hooks.md +126 -0
- package/hub/skills/tanstack-query/references/best-practices.md +241 -0
- package/hub/skills/tanstack-query/references/cache-strategies.md +474 -0
- package/hub/skills/tanstack-query/references/common-patterns.md +239 -0
- package/hub/skills/tanstack-query/references/migration-v5.md +93 -0
- package/hub/skills/tanstack-query/references/resilience-and-mutations.md +63 -0
- package/hub/skills/tanstack-query/references/testing.md +116 -0
- package/hub/skills/tanstack-query/references/top-errors.md +148 -0
- package/hub/skills/tanstack-query/references/typescript.md +176 -0
- package/hub/skills/tanstack-router/SKILL.md +145 -0
- package/hub/skills/tanstack-router/references/code-splitting.md +31 -0
- package/hub/skills/tanstack-router/references/errors-and-boundaries.md +44 -0
- package/hub/skills/tanstack-router/references/loaders-and-preload.md +51 -0
- package/hub/skills/tanstack-router/references/navigation.md +24 -0
- package/hub/skills/tanstack-router/references/private-routes.md +169 -0
- package/hub/skills/tanstack-router/references/router-context.md +35 -0
- package/hub/skills/tanstack-router/references/search-params.md +29 -0
- package/hub/skills/tanstack-router/references/typescript.md +24 -0
- package/hub/skills/testing/SKILL.md +187 -0
- package/hub/skills/testing/references/assertions.md +64 -0
- package/hub/skills/testing/references/async-testing.md +66 -0
- package/hub/skills/testing/references/e2e-strategy.md +69 -0
- package/hub/skills/testing/references/layer-matrix.md +67 -0
- package/hub/skills/testing/references/performance.md +49 -0
- package/hub/skills/testing/references/tooling-map.md +81 -0
- package/hub/skills/testing/references/zustand-mocking.md +84 -0
- package/hub/skills/typescript/SKILL.md +232 -0
- package/hub/skills/typescript/references/perf-additional-concerns.md +248 -0
- package/hub/skills/typescript/references/perf-execution-cache-locality.md +178 -0
- package/hub/skills/typescript/references/reduce-branching.md +147 -0
- package/hub/skills/typescript/references/reduce-looping.md +203 -0
- package/hub/skills/typescript/references/style-and-types.md +171 -0
- package/hub/skills/typescript/references/type-vs-interface.md +27 -0
- package/hub/skills/zod/SKILL.md +219 -0
- package/hub/skills/zustand/SKILL.md +273 -0
- package/package.json +59 -0
- package/tooling/cli/src/bin.js +11 -0
- package/tooling/cli/src/cli.js +409 -0
- package/tooling/cli/src/lib/catalog-loader.js +191 -0
- package/tooling/cli/src/lib/constants.js +39 -0
- package/tooling/cli/src/lib/errors.js +8 -0
- package/tooling/cli/src/lib/frontmatter.js +41 -0
- package/tooling/cli/src/lib/fs-utils.js +77 -0
- package/tooling/cli/src/lib/managed-header.js +74 -0
- package/tooling/cli/src/lib/manifest.js +105 -0
- package/tooling/cli/src/lib/resolver.js +53 -0
- package/tooling/cli/src/lib/sync-engine.js +262 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Common TanStack Query Patterns
|
|
2
|
+
|
|
3
|
+
**Reusable patterns for real-world applications**
|
|
4
|
+
|
|
5
|
+
> Alignment note: For required route data, follow the tanstack render-as-you-fetch flow
|
|
6
|
+
> (Application `queryOptions` → Loader `ensureQueryData` → Presentation `useSuspenseQuery`).
|
|
7
|
+
> Examples below use `useQuery` for brevity and are intended for non-route or optional data.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Pattern 1: Dependent Queries
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
function UserPosts({ userId }) {
|
|
15
|
+
const { data: user } = useQuery({
|
|
16
|
+
queryKey: ["users", userId],
|
|
17
|
+
queryFn: () => fetchUser(userId),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const { data: posts } = useQuery({
|
|
21
|
+
queryKey: ["users", userId, "posts"],
|
|
22
|
+
queryFn: () => fetchUserPosts(userId),
|
|
23
|
+
enabled: !!user, // Not for useSuspenseQuery
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Pattern 2: Parallel Queries with useQueries
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
function TodoDetails({ ids }) {
|
|
34
|
+
const results = useQueries({
|
|
35
|
+
queries: ids.map((id) => ({
|
|
36
|
+
queryKey: ["todos", id],
|
|
37
|
+
queryFn: () => fetchTodo(id),
|
|
38
|
+
})),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const isLoading = results.some((r) => r.isPending);
|
|
42
|
+
const data = results.map((r) => r.data);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Pattern 3: Paginated Queries with placeholderData
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { keepPreviousData } from "@tanstack/react-query";
|
|
52
|
+
|
|
53
|
+
function PaginatedTodos() {
|
|
54
|
+
const [page, setPage] = useState(0);
|
|
55
|
+
|
|
56
|
+
const { data } = useQuery({
|
|
57
|
+
queryKey: ["todos", page],
|
|
58
|
+
queryFn: () => fetchTodos(page),
|
|
59
|
+
placeholderData: keepPreviousData,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Pattern 4: Infinite Scroll
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
function InfiniteList() {
|
|
70
|
+
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
|
71
|
+
queryKey: ["items"],
|
|
72
|
+
queryFn: ({ pageParam }) => fetchItems(pageParam),
|
|
73
|
+
initialPageParam: 0,
|
|
74
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const ref = useRef();
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const observer = new IntersectionObserver(
|
|
80
|
+
([entry]) => entry.isIntersecting && hasNextPage && fetchNextPage()
|
|
81
|
+
);
|
|
82
|
+
if (ref.current) observer.observe(ref.current);
|
|
83
|
+
return () => observer.disconnect();
|
|
84
|
+
}, [fetchNextPage, hasNextPage]);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
{data.pages.map((page) => page.data.map((item) => <div>{item}</div>))}
|
|
89
|
+
<div ref={ref}>Loading...</div>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Pattern 5: Optimistic Updates
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
function useOptimisticToggle() {
|
|
101
|
+
const queryClient = useQueryClient();
|
|
102
|
+
|
|
103
|
+
return useMutation({
|
|
104
|
+
mutationFn: updateTodo,
|
|
105
|
+
onMutate: async (updated) => {
|
|
106
|
+
await queryClient.cancelQueries({ queryKey: ["todos"] });
|
|
107
|
+
const previous = queryClient.getQueryData(["todos"]);
|
|
108
|
+
|
|
109
|
+
queryClient.setQueryData(["todos"], (old) =>
|
|
110
|
+
old.map((todo) => (todo.id === updated.id ? updated : todo))
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return { previous };
|
|
114
|
+
},
|
|
115
|
+
onError: (err, vars, context) => {
|
|
116
|
+
queryClient.setQueryData(["todos"], context.previous);
|
|
117
|
+
},
|
|
118
|
+
onSettled: () => {
|
|
119
|
+
queryClient.invalidateQueries({ queryKey: ["todos"] });
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Pattern 6: Prefetching on Hover
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
function TodoList() {
|
|
131
|
+
const queryClient = useQueryClient();
|
|
132
|
+
|
|
133
|
+
const prefetch = (id) => {
|
|
134
|
+
queryClient.prefetchQuery({
|
|
135
|
+
queryKey: ["todos", id],
|
|
136
|
+
queryFn: () => fetchTodo(id),
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<ul>
|
|
142
|
+
{todos.map((todo) => (
|
|
143
|
+
<li onMouseEnter={() => prefetch(todo.id)}>
|
|
144
|
+
<Link to={`/todos/${todo.id}`}>{todo.title}</Link>
|
|
145
|
+
</li>
|
|
146
|
+
))}
|
|
147
|
+
</ul>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
> Use prefetching only for optional navigation, not required route data.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Pattern 7: Search/Debounce
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { useDeferredValue, useState } from "react";
|
|
160
|
+
|
|
161
|
+
function Search() {
|
|
162
|
+
const [search, setSearch] = useState("");
|
|
163
|
+
const deferredSearch = useDeferredValue(search);
|
|
164
|
+
|
|
165
|
+
const { data } = useQuery({
|
|
166
|
+
queryKey: ["search", deferredSearch],
|
|
167
|
+
queryFn: ({ signal }) =>
|
|
168
|
+
fetch(`/api/search?q=${deferredSearch}`, { signal }).then((r) => r.json()),
|
|
169
|
+
enabled: deferredSearch.length >= 2,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Pattern 8: Polling/Refetch Interval
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
const { data } = useQuery({
|
|
180
|
+
queryKey: ["stock-price"],
|
|
181
|
+
queryFn: fetchStockPrice,
|
|
182
|
+
refetchInterval: 1000 * 30,
|
|
183
|
+
refetchIntervalInBackground: true,
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Pattern 9: Conditional Fetching
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
const { data } = useQuery({
|
|
193
|
+
queryKey: ["user", userId],
|
|
194
|
+
queryFn: () => fetchUser(userId),
|
|
195
|
+
enabled: !!userId && isAuthenticated,
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Pattern 10: Initial Data from Cache
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
const { data: todo } = useQuery({
|
|
205
|
+
queryKey: ["todos", id],
|
|
206
|
+
queryFn: () => fetchTodo(id),
|
|
207
|
+
initialData: () => queryClient.getQueryData(["todos"])?.find((t) => t.id === id),
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Pattern 11: Mutation with Multiple Invalidations
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
useMutation({
|
|
217
|
+
mutationFn: updateTodo,
|
|
218
|
+
onSuccess: (updated) => {
|
|
219
|
+
queryClient.setQueryData(["todos", updated.id], updated);
|
|
220
|
+
queryClient.invalidateQueries({ queryKey: ["todos"] });
|
|
221
|
+
queryClient.invalidateQueries({ queryKey: ["stats"] });
|
|
222
|
+
queryClient.invalidateQueries({ queryKey: ["users", updated.userId] });
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Pattern 12: Error Boundaries with throwOnError
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
useQuery({
|
|
233
|
+
queryKey: ["todos"],
|
|
234
|
+
queryFn: fetchTodos,
|
|
235
|
+
throwOnError: true,
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
> For route data, prefer `throwOnError` + route error boundaries handled by the router.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# TanStack Query v4 → v5 Migration Guide
|
|
2
|
+
|
|
3
|
+
**Migration checklist for upgrading from React Query v4 to TanStack Query v5**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Breaking Changes Summary
|
|
8
|
+
|
|
9
|
+
### 1. Object Syntax Required
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
useQuery({
|
|
13
|
+
queryKey: ["todos"],
|
|
14
|
+
queryFn: fetchTodos,
|
|
15
|
+
staleTime: 5000,
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Query Callbacks Removed
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const { data } = useQuery({ queryKey: ["todos"], queryFn: fetchTodos });
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (data) console.log(data);
|
|
25
|
+
}, [data]);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 3. `isLoading` → `isPending`
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
const { isPending } = useQuery({ queryKey: ["todos"], queryFn: fetchTodos });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 4. `cacheTime` → `gcTime`
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
gcTime: 1000 * 60 * 60;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 5. `initialPageParam` Required for Infinite Queries
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
useInfiniteQuery({
|
|
44
|
+
queryKey: ["projects"],
|
|
45
|
+
queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
|
46
|
+
initialPageParam: 0,
|
|
47
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 6. `keepPreviousData` → `placeholderData`
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { keepPreviousData } from "@tanstack/react-query";
|
|
55
|
+
|
|
56
|
+
useQuery({
|
|
57
|
+
queryKey: ["todos", page],
|
|
58
|
+
queryFn: () => fetchTodos(page),
|
|
59
|
+
placeholderData: keepPreviousData,
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 7. `useErrorBoundary` → `throwOnError`
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
useQuery({
|
|
67
|
+
queryKey: ["todos"],
|
|
68
|
+
queryFn: fetchTodos,
|
|
69
|
+
throwOnError: true,
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Step-by-Step Migration
|
|
76
|
+
|
|
77
|
+
1. Update dependencies to the latest TanStack Query v5.
|
|
78
|
+
2. Replace function overloads with object syntax.
|
|
79
|
+
3. Replace query callbacks with `useEffect` or move to mutations.
|
|
80
|
+
4. Replace `isLoading` with `isPending`.
|
|
81
|
+
5. Replace `cacheTime` with `gcTime`.
|
|
82
|
+
6. Add `initialPageParam` to infinite queries.
|
|
83
|
+
7. Replace `keepPreviousData` with `placeholderData`.
|
|
84
|
+
8. Update error boundaries with `throwOnError`.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Common Migration Issues
|
|
89
|
+
|
|
90
|
+
- Callbacks not firing → use `useEffect`.
|
|
91
|
+
- `isLoading` always false → use `isPending`.
|
|
92
|
+
- `cacheTime` not recognized → use `gcTime`.
|
|
93
|
+
- Infinite query type error → add `initialPageParam`.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Resilience and mutations
|
|
2
|
+
|
|
3
|
+
Use this reference for robust mutation flows, rollback behavior, and user-facing feedback without breaking layer boundaries.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
- Query callbacks are removed in v5 queries, but mutation callbacks remain valid.
|
|
8
|
+
- Prefer optimistic updates with rollback for latency-sensitive UX.
|
|
9
|
+
- Keep feedback ownership in Presentation adapters; Application hooks should use callback injection or expose state.
|
|
10
|
+
- Preserve user-entered values on mutation failure for retry.
|
|
11
|
+
|
|
12
|
+
## Pattern A: optimistic update + rollback
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
useMutation({
|
|
16
|
+
mutationFn: updateTodo,
|
|
17
|
+
onMutate: async (nextTodo) => {
|
|
18
|
+
await queryClient.cancelQueries({ queryKey: ["todos"] });
|
|
19
|
+
const previous = queryClient.getQueryData(["todos"]);
|
|
20
|
+
queryClient.setQueryData(["todos"], (old: Todo[] = []) =>
|
|
21
|
+
old.map((todo) => (todo.id === nextTodo.id ? nextTodo : todo))
|
|
22
|
+
);
|
|
23
|
+
return { previous };
|
|
24
|
+
},
|
|
25
|
+
onError: (_error, _vars, context) => {
|
|
26
|
+
queryClient.setQueryData(["todos"], context?.previous);
|
|
27
|
+
},
|
|
28
|
+
onSettled: () => {
|
|
29
|
+
queryClient.invalidateQueries({ queryKey: ["todos"] });
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Pattern B: layer-safe feedback bridge
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
export const useUpdateProduct = (onErrorFeedback: (message: string) => void) => {
|
|
38
|
+
return useMutation({
|
|
39
|
+
mutationFn: ProductRepository.update,
|
|
40
|
+
onError: () => {
|
|
41
|
+
onErrorFeedback("Failed to update product. Please retry.");
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Pattern C: throwOnError strategy for reads
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
useQuery({
|
|
51
|
+
queryKey: ["products"],
|
|
52
|
+
queryFn: fetchProducts,
|
|
53
|
+
throwOnError: (error) => error.status >= 500,
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Use route boundaries for route-critical reads and local handling for optional/background reads.
|
|
58
|
+
|
|
59
|
+
## Pitfalls
|
|
60
|
+
|
|
61
|
+
- Treating query callback removal as mutation callback removal.
|
|
62
|
+
- Resetting forms on mutation error and losing user state.
|
|
63
|
+
- Importing UI toast adapters directly in Application hooks.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Testing TanStack Query
|
|
2
|
+
|
|
3
|
+
**Testing queries, mutations, and components**
|
|
4
|
+
|
|
5
|
+
> Alignment note: for route data, test loaders with `ensureQueryData` and render pages with `useSuspenseQuery`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Test Utils
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
13
|
+
import { render } from "@testing-library/react";
|
|
14
|
+
|
|
15
|
+
export function createTestQueryClient() {
|
|
16
|
+
return new QueryClient({
|
|
17
|
+
defaultOptions: {
|
|
18
|
+
queries: {
|
|
19
|
+
retry: false,
|
|
20
|
+
gcTime: Infinity,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
logger: {
|
|
24
|
+
log: console.log,
|
|
25
|
+
warn: console.warn,
|
|
26
|
+
error: () => {},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderWithClient(ui: React.ReactElement) {
|
|
32
|
+
const testQueryClient = createTestQueryClient();
|
|
33
|
+
return render(<QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider>);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Testing Queries
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
43
|
+
|
|
44
|
+
describe("useTodos", () => {
|
|
45
|
+
it("fetches todos successfully", async () => {
|
|
46
|
+
const { result } = renderHook(() => useTodos(), {
|
|
47
|
+
wrapper: ({ children }) => (
|
|
48
|
+
<QueryClientProvider client={createTestQueryClient()}>{children}</QueryClientProvider>
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result.current.isPending).toBe(true);
|
|
53
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Testing with MSW
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { HttpResponse, http } from "msw";
|
|
64
|
+
import { setupServer } from "msw/node";
|
|
65
|
+
|
|
66
|
+
const server = setupServer(
|
|
67
|
+
http.get("/api/todos", () => {
|
|
68
|
+
return HttpResponse.json([{ id: 1, title: "Test todo", completed: false }]);
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Testing Mutations
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
test("adds todo successfully", async () => {
|
|
79
|
+
const { result } = renderHook(() => useAddTodo());
|
|
80
|
+
|
|
81
|
+
act(() => {
|
|
82
|
+
result.current.mutate({ title: "New todo" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Testing Components with Suspense
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { Suspense } from "react";
|
|
95
|
+
|
|
96
|
+
render(
|
|
97
|
+
<QueryClientProvider client={createTestQueryClient()}>
|
|
98
|
+
<Suspense fallback={null}>
|
|
99
|
+
<TodoList />
|
|
100
|
+
</Suspense>
|
|
101
|
+
</QueryClientProvider>
|
|
102
|
+
);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Best Practices
|
|
108
|
+
|
|
109
|
+
✅ Disable retries in tests
|
|
110
|
+
✅ Use MSW for consistent mocking
|
|
111
|
+
✅ Test loading, success, and error states
|
|
112
|
+
✅ Test optimistic updates and rollbacks
|
|
113
|
+
✅ Use `waitFor` for async updates
|
|
114
|
+
✅ Prefill cache when testing with existing data
|
|
115
|
+
❌ Don't test implementation details
|
|
116
|
+
❌ Don't mock TanStack Query internals
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Top TanStack Query Errors & Solutions
|
|
2
|
+
|
|
3
|
+
**Complete error reference with fixes**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Error #1: Object Syntax Required
|
|
8
|
+
|
|
9
|
+
**Why**: v5 removed function overloads; only object syntax works.
|
|
10
|
+
|
|
11
|
+
**Fix**:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
useQuery({ queryKey: ["todos"], queryFn: fetchTodos });
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Error #2: Query Callbacks Not Working
|
|
20
|
+
|
|
21
|
+
**Why**: `onSuccess`, `onError`, `onSettled` removed from queries (still work in mutations).
|
|
22
|
+
|
|
23
|
+
**Fix**:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
const { data } = useQuery({ queryKey: ["todos"], queryFn: fetchTodos });
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (data) console.log(data);
|
|
29
|
+
}, [data]);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Error #3: isLoading Always False
|
|
35
|
+
|
|
36
|
+
**Why**: v5 changed `isLoading` meaning; use `isPending` for initial load.
|
|
37
|
+
|
|
38
|
+
**Fix**:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
const { isPending } = useQuery({ queryKey: ["todos"], queryFn: fetchTodos });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Error #4: cacheTime Not Recognized
|
|
47
|
+
|
|
48
|
+
**Why**: Renamed to `gcTime`.
|
|
49
|
+
|
|
50
|
+
**Fix**:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
gcTime: 1000 * 60 * 60;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Error #5: useSuspenseQuery + enabled
|
|
59
|
+
|
|
60
|
+
**Why**: `enabled` not available with Suspense.
|
|
61
|
+
|
|
62
|
+
**Fix**:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
{
|
|
66
|
+
id ? <TodoComponent id={id} /> : <div>No ID</div>;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Error #6: initialPageParam Required
|
|
73
|
+
|
|
74
|
+
**Why**: v5 requires explicit `initialPageParam` for infinite queries.
|
|
75
|
+
|
|
76
|
+
**Fix**:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
useInfiniteQuery({
|
|
80
|
+
queryKey: ["projects"],
|
|
81
|
+
queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
|
82
|
+
initialPageParam: 0,
|
|
83
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Error #7: keepPreviousData Not Working
|
|
90
|
+
|
|
91
|
+
**Why**: replaced by `placeholderData`.
|
|
92
|
+
|
|
93
|
+
**Fix**:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { keepPreviousData } from "@tanstack/react-query";
|
|
97
|
+
|
|
98
|
+
useQuery({
|
|
99
|
+
queryKey: ["todos", page],
|
|
100
|
+
queryFn: () => fetchTodos(page),
|
|
101
|
+
placeholderData: keepPreviousData,
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Error #8: Error Type Mismatch
|
|
108
|
+
|
|
109
|
+
**Why**: v5 defaults to `Error` instead of `unknown`.
|
|
110
|
+
|
|
111
|
+
**Fix**:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
const { error } = useQuery<DataType, string>({
|
|
115
|
+
queryKey: ["data"],
|
|
116
|
+
queryFn: async () => {
|
|
117
|
+
if (fail) throw "custom error";
|
|
118
|
+
return data;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Error #9: invalidateQueries Doesn’t Refetch Inactive
|
|
126
|
+
|
|
127
|
+
**Why**: `invalidateQueries` only refetches active queries by default.
|
|
128
|
+
|
|
129
|
+
**Fix**:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
queryClient.invalidateQueries({
|
|
133
|
+
queryKey: ["todos"],
|
|
134
|
+
refetchType: "all",
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Quick Diagnosis Checklist
|
|
141
|
+
|
|
142
|
+
- [ ] Object syntax in all hooks?
|
|
143
|
+
- [ ] `isPending` for initial loading?
|
|
144
|
+
- [ ] `gcTime` instead of `cacheTime`?
|
|
145
|
+
- [ ] No query callbacks (`onSuccess`, etc.)?
|
|
146
|
+
- [ ] `initialPageParam` for infinite queries?
|
|
147
|
+
- [ ] Query keys are arrays?
|
|
148
|
+
- [ ] Errors thrown in `queryFn`?
|