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,241 @@
|
|
|
1
|
+
# TanStack Query Best Practices
|
|
2
|
+
|
|
3
|
+
**Performance, caching strategies, and common patterns**
|
|
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
|
+
## 1. Avoid Request Waterfalls
|
|
12
|
+
|
|
13
|
+
### ❌ Bad: Sequential Dependencies
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
function BadUserProfile({ userId }) {
|
|
17
|
+
const { data: user } = useQuery({
|
|
18
|
+
queryKey: ["users", userId],
|
|
19
|
+
queryFn: () => fetchUser(userId),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const { data: posts } = useQuery({
|
|
23
|
+
queryKey: ["posts", user?.id],
|
|
24
|
+
queryFn: () => fetchPosts(user!.id),
|
|
25
|
+
enabled: !!user,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const { data: comments } = useQuery({
|
|
29
|
+
queryKey: ["comments", posts?.[0]?.id],
|
|
30
|
+
queryFn: () => fetchComments(posts![0].id),
|
|
31
|
+
enabled: !!posts && posts.length > 0,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### ✅ Good: Parallel Queries
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
function GoodUserProfile({ userId }) {
|
|
40
|
+
const { data: user } = useQuery({
|
|
41
|
+
queryKey: ["users", userId],
|
|
42
|
+
queryFn: () => fetchUser(userId),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const { data: posts } = useQuery({
|
|
46
|
+
queryKey: ["posts", userId],
|
|
47
|
+
queryFn: () => fetchPosts(userId),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const { data: comments } = useQuery({
|
|
51
|
+
queryKey: ["comments", userId],
|
|
52
|
+
queryFn: () => fetchUserComments(userId),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> For route data, prefetch in parallel inside the loader (e.g., `Promise.all`).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 2. Query Key Strategy
|
|
62
|
+
|
|
63
|
+
### Hierarchical Structure
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
["todos"][("todos", { status: "done" })][("todos", 123)];
|
|
67
|
+
|
|
68
|
+
queryClient.invalidateQueries({ queryKey: ["todos"] });
|
|
69
|
+
queryClient.invalidateQueries({ queryKey: ["todos", { status: "done" }] });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Best Practices
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// ✅ Good: Stable, serializable keys
|
|
76
|
+
["users", userId, { sort: "name", filter: "active" }][
|
|
77
|
+
// ❌ Bad: Functions in keys (not serializable)
|
|
78
|
+
("users", () => userId)
|
|
79
|
+
][
|
|
80
|
+
// ❌ Bad: Changing order
|
|
81
|
+
("users", { filter: "active", sort: "name" })
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// ✅ Good: Consistent ordering
|
|
85
|
+
const userFilters = { filter: "active", sort: "name" };
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 3. Caching Configuration
|
|
91
|
+
|
|
92
|
+
### staleTime vs gcTime
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// Real-time data
|
|
96
|
+
staleTime: 0;
|
|
97
|
+
// Stable data
|
|
98
|
+
staleTime: 1000 * 60 * 60;
|
|
99
|
+
// Static data
|
|
100
|
+
staleTime: Infinity;
|
|
101
|
+
|
|
102
|
+
// Keep frequently revisited data longer
|
|
103
|
+
gcTime: 1000 * 60 * 60;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Per-Query vs Global
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
const queryClient = new QueryClient({
|
|
110
|
+
defaultOptions: {
|
|
111
|
+
queries: {
|
|
112
|
+
staleTime: 1000 * 60 * 5,
|
|
113
|
+
gcTime: 1000 * 60 * 60,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
useQuery({
|
|
119
|
+
queryKey: ["stock-price"],
|
|
120
|
+
queryFn: fetchStockPrice,
|
|
121
|
+
staleTime: 0,
|
|
122
|
+
refetchInterval: 1000 * 30,
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 4. Use queryOptions Factory
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
export const todosQueryOptions = queryOptions({
|
|
132
|
+
queryKey: ["todos"],
|
|
133
|
+
queryFn: fetchTodos,
|
|
134
|
+
staleTime: 1000 * 60,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
useQuery(todosQueryOptions);
|
|
138
|
+
useSuspenseQuery(todosQueryOptions);
|
|
139
|
+
queryClient.prefetchQuery(todosQueryOptions);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 5. Data Transformations
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
function TodoCount() {
|
|
148
|
+
const { data: count } = useQuery({
|
|
149
|
+
queryKey: ["todos"],
|
|
150
|
+
queryFn: fetchTodos,
|
|
151
|
+
select: (data) => data.length,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 6. Prefetching (Optional Navigation)
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
const prefetch = (id: number) => {
|
|
162
|
+
queryClient.prefetchQuery({
|
|
163
|
+
queryKey: ["todos", id],
|
|
164
|
+
queryFn: () => fetchTodo(id),
|
|
165
|
+
staleTime: 1000 * 60 * 5,
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
> Do not use `prefetchQuery` for required route data; use loader `ensureQueryData` instead.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 7. Optimistic Updates
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
useMutation({
|
|
178
|
+
mutationFn: updateTodo,
|
|
179
|
+
onMutate: async (newTodo) => {
|
|
180
|
+
await queryClient.cancelQueries({ queryKey: ["todos"] });
|
|
181
|
+
const previous = queryClient.getQueryData(["todos"]);
|
|
182
|
+
queryClient.setQueryData(["todos"], (old) => [...(old ?? []), newTodo]);
|
|
183
|
+
return { previous };
|
|
184
|
+
},
|
|
185
|
+
onError: (err, newTodo, context) => {
|
|
186
|
+
queryClient.setQueryData(["todos"], context?.previous);
|
|
187
|
+
},
|
|
188
|
+
onSettled: () => {
|
|
189
|
+
queryClient.invalidateQueries({ queryKey: ["todos"] });
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 8. Error Handling Strategy
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
useQuery({
|
|
200
|
+
queryKey: ["todos"],
|
|
201
|
+
queryFn: fetchTodos,
|
|
202
|
+
throwOnError: true,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
useQuery({
|
|
206
|
+
queryKey: ["todos"],
|
|
207
|
+
queryFn: fetchTodos,
|
|
208
|
+
throwOnError: (error) => error.status >= 500,
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 9. Server State vs Client State
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
// ✅ Server state
|
|
218
|
+
const { data: todos } = useQuery({ queryKey: ["todos"], queryFn: fetchTodos });
|
|
219
|
+
|
|
220
|
+
// ✅ Client state
|
|
221
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 10. Performance Monitoring
|
|
227
|
+
|
|
228
|
+
Use DevTools to:
|
|
229
|
+
|
|
230
|
+
- Verify cache hits
|
|
231
|
+
- Track refetch frequency
|
|
232
|
+
- Inspect query states
|
|
233
|
+
- Export cache for debugging
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 11. Mutation Resilience Ownership
|
|
238
|
+
|
|
239
|
+
- Mutation callbacks are valid in v5 and should be used for rollback and invalidation.
|
|
240
|
+
- Keep UI feedback adapters in Presentation; prefer callback injection in Application hooks.
|
|
241
|
+
- For complete mutation resilience patterns, see `references/resilience-and-mutations.md`.
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
# Cache Strategies & Invalidation Patterns
|
|
2
|
+
|
|
3
|
+
Comprehensive guide to TanStack Query v5 caching, invalidation, and data synchronization.
|
|
4
|
+
|
|
5
|
+
> Alignment note: For required route data, follow the tanstack render-as-you-fetch flow
|
|
6
|
+
> (Application `queryOptions` → Loader `ensureQueryData` → Presentation `useSuspenseQuery`).
|
|
7
|
+
> The `useQuery` examples below are conceptual and should be adapted to that flow when used in routes.
|
|
8
|
+
|
|
9
|
+
## Cache Lifecycle
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
13
|
+
│ Query Cache Lifecycle │
|
|
14
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
15
|
+
│ │
|
|
16
|
+
│ ┌──────────┐ staleTime ┌──────────┐ gcTime ┌──────────────┐ │
|
|
17
|
+
│ │ FRESH │ ──────────────► │ STALE │ ────────────► │ GARBAGE │ │
|
|
18
|
+
│ │ │ (no refetch) │ │ (if unused) │ COLLECTED │ │
|
|
19
|
+
│ └──────────┘ └──────────┘ └──────────────┘ │
|
|
20
|
+
│ │ │ │
|
|
21
|
+
│ │ Component mounts │ Component mounts │
|
|
22
|
+
│ ▼ ▼ │
|
|
23
|
+
│ Return cached data Return cached data │
|
|
24
|
+
│ (no network request) + background refetch │
|
|
25
|
+
│ │
|
|
26
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## staleTime vs gcTime
|
|
30
|
+
|
|
31
|
+
| Setting | Purpose | Default | When to Adjust |
|
|
32
|
+
| ----------- | ------------------------------------ | ---------------- | --------------------------------------- |
|
|
33
|
+
| `staleTime` | How long data is considered "fresh" | 0 (always stale) | Increase for rarely-changing data |
|
|
34
|
+
| `gcTime` | How long unused data stays in memory | 5 minutes | Increase for frequently revisited pages |
|
|
35
|
+
|
|
36
|
+
### staleTime Configuration
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Real-time data (stock prices, notifications)
|
|
40
|
+
useQuery({
|
|
41
|
+
queryKey: ["stock", symbol],
|
|
42
|
+
queryFn: () => fetchStock(symbol),
|
|
43
|
+
staleTime: 0, // Always refetch
|
|
44
|
+
refetchInterval: 5000, // Poll every 5s
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// User profile (changes occasionally)
|
|
48
|
+
useQuery({
|
|
49
|
+
queryKey: ["user", userId],
|
|
50
|
+
queryFn: () => fetchUser(userId),
|
|
51
|
+
staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Static configuration (rarely changes)
|
|
55
|
+
useQuery({
|
|
56
|
+
queryKey: ["config"],
|
|
57
|
+
queryFn: fetchConfig,
|
|
58
|
+
staleTime: 60 * 60 * 1000, // Fresh for 1 hour
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Truly static data (never changes)
|
|
62
|
+
useQuery({
|
|
63
|
+
queryKey: ["countries"],
|
|
64
|
+
queryFn: fetchCountries,
|
|
65
|
+
staleTime: Infinity, // Never refetch
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### gcTime Configuration
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Frequently revisited pages (keep in memory longer)
|
|
73
|
+
useQuery({
|
|
74
|
+
queryKey: ["dashboard"],
|
|
75
|
+
queryFn: fetchDashboard,
|
|
76
|
+
gcTime: 30 * 60 * 1000, // Keep 30 minutes after unmount
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Large data that shouldn't linger
|
|
80
|
+
useQuery({
|
|
81
|
+
queryKey: ["reports", year],
|
|
82
|
+
queryFn: () => fetchReports(year),
|
|
83
|
+
gcTime: 60 * 1000, // Clear 1 minute after unmount
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Critical data (keep indefinitely)
|
|
87
|
+
useQuery({
|
|
88
|
+
queryKey: ["currentUser"],
|
|
89
|
+
queryFn: fetchCurrentUser,
|
|
90
|
+
gcTime: Infinity, // Never garbage collect
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Query Key Hierarchy
|
|
95
|
+
|
|
96
|
+
Query keys form a hierarchy. Invalidating a parent invalidates all children.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Key hierarchy:
|
|
100
|
+
// ['todos']
|
|
101
|
+
// └── ['todos', 'list']
|
|
102
|
+
// └── ['todos', 'list', { filter: 'active' }]
|
|
103
|
+
// └── ['todos', 'list', { filter: 'completed' }]
|
|
104
|
+
// └── ['todos', 'detail', '1']
|
|
105
|
+
// └── ['todos', 'detail', '2']
|
|
106
|
+
|
|
107
|
+
// Invalidate ALL todo queries (list + all details)
|
|
108
|
+
queryClient.invalidateQueries({ queryKey: ["todos"] });
|
|
109
|
+
|
|
110
|
+
// Invalidate only list queries (not details)
|
|
111
|
+
queryClient.invalidateQueries({ queryKey: ["todos", "list"] });
|
|
112
|
+
|
|
113
|
+
// Invalidate specific filter
|
|
114
|
+
queryClient.invalidateQueries({
|
|
115
|
+
queryKey: ["todos", "list", { filter: "active" }],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Invalidate exact key only (not children)
|
|
119
|
+
queryClient.invalidateQueries({
|
|
120
|
+
queryKey: ["todos", "list"],
|
|
121
|
+
exact: true,
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Invalidation Strategies
|
|
126
|
+
|
|
127
|
+
### 1. Mutation-Based Invalidation
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// ✅ RECOMMENDED: Invalidate related queries after mutation
|
|
131
|
+
const createTodo = useMutation({
|
|
132
|
+
mutationFn: api.createTodo,
|
|
133
|
+
onSuccess: () => {
|
|
134
|
+
// Invalidate list to include new item
|
|
135
|
+
queryClient.invalidateQueries({ queryKey: ["todos", "list"] });
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const updateTodo = useMutation({
|
|
140
|
+
mutationFn: api.updateTodo,
|
|
141
|
+
onSuccess: (data, variables) => {
|
|
142
|
+
// Invalidate specific item + list
|
|
143
|
+
queryClient.invalidateQueries({
|
|
144
|
+
queryKey: ["todos", "detail", variables.id],
|
|
145
|
+
});
|
|
146
|
+
queryClient.invalidateQueries({ queryKey: ["todos", "list"] });
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const deleteTodo = useMutation({
|
|
151
|
+
mutationFn: api.deleteTodo,
|
|
152
|
+
onSuccess: (_, id) => {
|
|
153
|
+
// Remove from cache entirely
|
|
154
|
+
queryClient.removeQueries({ queryKey: ["todos", "detail", id] });
|
|
155
|
+
// Invalidate list
|
|
156
|
+
queryClient.invalidateQueries({ queryKey: ["todos", "list"] });
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 2. Optimistic Updates with Reconciliation
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const updateTodo = useMutation({
|
|
165
|
+
mutationFn: ({ id, ...data }) => api.updateTodo(id, data),
|
|
166
|
+
|
|
167
|
+
// Step 1: Cancel any outgoing refetches
|
|
168
|
+
onMutate: async ({ id, ...updates }) => {
|
|
169
|
+
await queryClient.cancelQueries({ queryKey: ["todos", "detail", id] });
|
|
170
|
+
await queryClient.cancelQueries({ queryKey: ["todos", "list"] });
|
|
171
|
+
|
|
172
|
+
// Step 2: Snapshot current state
|
|
173
|
+
const previousTodo = queryClient.getQueryData(["todos", "detail", id]);
|
|
174
|
+
const previousList = queryClient.getQueryData(["todos", "list"]);
|
|
175
|
+
|
|
176
|
+
// Step 3: Optimistically update both caches
|
|
177
|
+
queryClient.setQueryData(["todos", "detail", id], (old) =>
|
|
178
|
+
old ? { ...old, ...updates } : undefined
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
queryClient.setQueryData(["todos", "list"], (old) =>
|
|
182
|
+
old?.map((todo) => (todo.id === id ? { ...todo, ...updates } : todo))
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Step 4: Return context for rollback
|
|
186
|
+
return { previousTodo, previousList, id };
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Step 5: Rollback on error
|
|
190
|
+
onError: (err, variables, context) => {
|
|
191
|
+
if (context) {
|
|
192
|
+
queryClient.setQueryData(["todos", "detail", context.id], context.previousTodo);
|
|
193
|
+
queryClient.setQueryData(["todos", "list"], context.previousList);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Step 6: Always reconcile with server
|
|
198
|
+
onSettled: (data, error, { id }) => {
|
|
199
|
+
queryClient.invalidateQueries({ queryKey: ["todos", "detail", id] });
|
|
200
|
+
queryClient.invalidateQueries({ queryKey: ["todos", "list"] });
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 3. Predicate-Based Invalidation
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// Invalidate todos by status
|
|
209
|
+
queryClient.invalidateQueries({
|
|
210
|
+
predicate: (query) => {
|
|
211
|
+
const key = query.queryKey;
|
|
212
|
+
return (
|
|
213
|
+
key[0] === "todos" &&
|
|
214
|
+
key[1] === "list" &&
|
|
215
|
+
(key[2] as { filter?: string })?.filter === "completed"
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Invalidate all queries older than 10 minutes
|
|
221
|
+
queryClient.invalidateQueries({
|
|
222
|
+
predicate: (query) => {
|
|
223
|
+
const dataUpdatedAt = query.state.dataUpdatedAt;
|
|
224
|
+
return Date.now() - dataUpdatedAt > 10 * 60 * 1000;
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Invalidate queries with errors
|
|
229
|
+
queryClient.invalidateQueries({
|
|
230
|
+
predicate: (query) => query.state.status === "error",
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 4. Type-Based Invalidation
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// Invalidate only active queries (currently rendered)
|
|
238
|
+
queryClient.invalidateQueries({ refetchType: "active" });
|
|
239
|
+
|
|
240
|
+
// Invalidate inactive queries (not rendered but in cache)
|
|
241
|
+
queryClient.invalidateQueries({ refetchType: "inactive" });
|
|
242
|
+
|
|
243
|
+
// Invalidate all queries
|
|
244
|
+
queryClient.invalidateQueries({ refetchType: "all" });
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 5. Refetch vs Invalidate
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// invalidateQueries: Mark as stale, refetch if active
|
|
251
|
+
queryClient.invalidateQueries({ queryKey: ["todos"] });
|
|
252
|
+
// - Marks all matching queries as stale
|
|
253
|
+
// - Active queries refetch immediately
|
|
254
|
+
// - Inactive queries refetch on next mount
|
|
255
|
+
|
|
256
|
+
// refetchQueries: Force immediate refetch
|
|
257
|
+
queryClient.refetchQueries({ queryKey: ["todos"], type: "active" });
|
|
258
|
+
// - Forces refetch regardless of stale state
|
|
259
|
+
// - Only refetches specified type
|
|
260
|
+
|
|
261
|
+
// When to use each:
|
|
262
|
+
// invalidateQueries: After mutations (let React Query decide when to refetch)
|
|
263
|
+
// refetchQueries: When you need guaranteed fresh data NOW
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Direct Cache Manipulation
|
|
267
|
+
|
|
268
|
+
### setQueryData
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// Update single item
|
|
272
|
+
queryClient.setQueryData(["user", userId], (old) =>
|
|
273
|
+
old ? { ...old, name: "New Name" } : undefined
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Add item to list
|
|
277
|
+
queryClient.setQueryData(["todos", "list"], (old) => (old ? [...old, newTodo] : [newTodo]));
|
|
278
|
+
|
|
279
|
+
// Update item in list
|
|
280
|
+
queryClient.setQueryData(["todos", "list"], (old) =>
|
|
281
|
+
old?.map((todo) => (todo.id === updatedTodo.id ? updatedTodo : todo))
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Remove item from list
|
|
285
|
+
queryClient.setQueryData(["todos", "list"], (old) => old?.filter((todo) => todo.id !== deletedId));
|
|
286
|
+
|
|
287
|
+
// ⚠️ IMPORTANT: Always return new reference
|
|
288
|
+
// ❌ BAD: Mutating existing data
|
|
289
|
+
queryClient.setQueryData(["todos"], (old) => {
|
|
290
|
+
old?.push(newTodo); // Mutation!
|
|
291
|
+
return old;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ✅ GOOD: Return new array
|
|
295
|
+
queryClient.setQueryData(["todos"], (old) => (old ? [...old, newTodo] : [newTodo]));
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### getQueryData & getQueriesData
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// Get single query data
|
|
302
|
+
const user = queryClient.getQueryData<User>(["user", userId]);
|
|
303
|
+
|
|
304
|
+
// Get all matching queries
|
|
305
|
+
const allTodoQueries = queryClient.getQueriesData<Todo[]>({
|
|
306
|
+
queryKey: ["todos"],
|
|
307
|
+
});
|
|
308
|
+
// Returns: Array<[queryKey, data]>
|
|
309
|
+
|
|
310
|
+
// Check if data exists
|
|
311
|
+
const hasUser = queryClient.getQueryData(["user", userId]) !== undefined;
|
|
312
|
+
|
|
313
|
+
// Get query state (includes status, error, etc.)
|
|
314
|
+
const state = queryClient.getQueryState(["user", userId]);
|
|
315
|
+
if (state?.status === "error") {
|
|
316
|
+
console.log("Query failed:", state.error);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### removeQueries
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// Remove specific query
|
|
324
|
+
queryClient.removeQueries({ queryKey: ["todos", "detail", deletedId] });
|
|
325
|
+
|
|
326
|
+
// Remove all queries matching prefix
|
|
327
|
+
queryClient.removeQueries({ queryKey: ["todos"] });
|
|
328
|
+
|
|
329
|
+
// Remove inactive queries only
|
|
330
|
+
queryClient.removeQueries({ queryKey: ["todos"], type: "inactive" });
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Cache Persistence
|
|
334
|
+
|
|
335
|
+
### persist with localStorage
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
|
339
|
+
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
|
|
340
|
+
|
|
341
|
+
const queryClient = new QueryClient({
|
|
342
|
+
defaultOptions: {
|
|
343
|
+
queries: {
|
|
344
|
+
gcTime: 1000 * 60 * 60 * 24, // 24 hours (must be >= maxAge)
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const persister = createSyncStoragePersister({
|
|
350
|
+
storage: window.localStorage,
|
|
351
|
+
key: 'REACT_QUERY_CACHE',
|
|
352
|
+
throttleTime: 1000,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
function App() {
|
|
356
|
+
return (
|
|
357
|
+
<PersistQueryClientProvider
|
|
358
|
+
client={queryClient}
|
|
359
|
+
persistOptions={{
|
|
360
|
+
persister,
|
|
361
|
+
maxAge: 1000 * 60 * 60 * 24, // 24 hours
|
|
362
|
+
dehydrateOptions: {
|
|
363
|
+
shouldDehydrateQuery: (query) => {
|
|
364
|
+
// Only persist successful queries
|
|
365
|
+
return query.state.status === 'success';
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
}}
|
|
369
|
+
>
|
|
370
|
+
<YourApp />
|
|
371
|
+
</PersistQueryClientProvider>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Async Persistence (IndexedDB)
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
|
|
380
|
+
import { del, get, set } from "idb-keyval";
|
|
381
|
+
|
|
382
|
+
const persister = createAsyncStoragePersister({
|
|
383
|
+
storage: {
|
|
384
|
+
getItem: async (key) => await get(key),
|
|
385
|
+
setItem: async (key, value) => await set(key, value),
|
|
386
|
+
removeItem: async (key) => await del(key),
|
|
387
|
+
},
|
|
388
|
+
key: "REACT_QUERY_CACHE",
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Real-Time Data Strategies
|
|
393
|
+
|
|
394
|
+
### Polling
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
useQuery({
|
|
398
|
+
queryKey: ["notifications"],
|
|
399
|
+
queryFn: fetchNotifications,
|
|
400
|
+
refetchInterval: 30000, // Poll every 30s
|
|
401
|
+
refetchIntervalInBackground: false, // Pause when tab hidden
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### WebSocket Integration
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// Subscribe to WebSocket updates
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
const ws = new WebSocket("wss://api.example.com/ws");
|
|
411
|
+
|
|
412
|
+
ws.onmessage = (event) => {
|
|
413
|
+
const data = JSON.parse(event.data);
|
|
414
|
+
|
|
415
|
+
if (data.type === "TODO_UPDATED") {
|
|
416
|
+
// Update cache directly
|
|
417
|
+
queryClient.setQueryData(["todos", data.todo.id], data.todo);
|
|
418
|
+
// Invalidate list to ensure consistency
|
|
419
|
+
queryClient.invalidateQueries({ queryKey: ["todos", "list"] });
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return () => ws.close();
|
|
424
|
+
}, [queryClient]);
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Server-Sent Events (SSE)
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
useEffect(() => {
|
|
431
|
+
const eventSource = new EventSource("/api/events");
|
|
432
|
+
|
|
433
|
+
eventSource.addEventListener("cache-invalidation", (event) => {
|
|
434
|
+
const { queryKey } = JSON.parse(event.data);
|
|
435
|
+
queryClient.invalidateQueries({ queryKey });
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return () => eventSource.close();
|
|
439
|
+
}, [queryClient]);
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Common Patterns Matrix
|
|
443
|
+
|
|
444
|
+
| Scenario | staleTime | gcTime | Refetch Strategy |
|
|
445
|
+
| ------------------------ | --------- | -------- | ------------------- |
|
|
446
|
+
| Real-time (stocks, chat) | 0 | 5min | Poll or WebSocket |
|
|
447
|
+
| User data | 5min | 30min | Window focus |
|
|
448
|
+
| Product catalog | 1min | 10min | On navigation |
|
|
449
|
+
| Static config | Infinity | Infinity | Manual/deploy |
|
|
450
|
+
| Search results | 0 | 1min | On input change |
|
|
451
|
+
| Dashboard | 30s | 5min | Poll + window focus |
|
|
452
|
+
|
|
453
|
+
## Debugging Tips
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
// Log all query activity in development
|
|
457
|
+
if (process.env.NODE_ENV === "development") {
|
|
458
|
+
queryClient.getQueryCache().subscribe((event) => {
|
|
459
|
+
console.log("Query event:", event.type, event.query.queryKey);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Inspect query state
|
|
464
|
+
const queryCache = queryClient.getQueryCache();
|
|
465
|
+
const queries = queryCache.getAll();
|
|
466
|
+
queries.forEach((query) => {
|
|
467
|
+
console.log({
|
|
468
|
+
key: query.queryKey,
|
|
469
|
+
state: query.state.status,
|
|
470
|
+
dataUpdatedAt: new Date(query.state.dataUpdatedAt),
|
|
471
|
+
isStale: query.isStale(),
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
```
|