@react-vault/create-app 0.1.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 +12 -0
- package/README.md +16 -0
- package/bin/create-app.js +8 -0
- package/claude-toolkit/README.md +131 -0
- package/claude-toolkit/agents/bfsi-accessibility-auditor.md +132 -0
- package/claude-toolkit/agents/bfsi-architect.md +156 -0
- package/claude-toolkit/agents/bfsi-code-reviewer.md +137 -0
- package/claude-toolkit/agents/bfsi-compliance-auditor.md +161 -0
- package/claude-toolkit/agents/bfsi-pii-scanner.md +142 -0
- package/claude-toolkit/agents/bfsi-pr-reviewer.md +114 -0
- package/claude-toolkit/agents/bfsi-security-reviewer.md +136 -0
- package/claude-toolkit/commands/bfsi-audit.md +46 -0
- package/claude-toolkit/commands/bfsi-doctor.md +97 -0
- package/claude-toolkit/commands/bfsi-review.md +46 -0
- package/claude-toolkit/commands/bfsi-scaffold.md +47 -0
- package/claude-toolkit/hooks/hooks.json +181 -0
- package/claude-toolkit/hooks/scripts/a11y-check.sh +63 -0
- package/claude-toolkit/hooks/scripts/audit-prompt.sh +36 -0
- package/claude-toolkit/hooks/scripts/block-destructive.sh +41 -0
- package/claude-toolkit/hooks/scripts/block-force-push.sh +30 -0
- package/claude-toolkit/hooks/scripts/format.sh +42 -0
- package/claude-toolkit/hooks/scripts/inject-context.sh +44 -0
- package/claude-toolkit/hooks/scripts/lint.sh +45 -0
- package/claude-toolkit/hooks/scripts/protect-files.sh +53 -0
- package/claude-toolkit/hooks/scripts/save-compliance-context.sh +35 -0
- package/claude-toolkit/hooks/scripts/scan-pii.sh +87 -0
- package/claude-toolkit/hooks/scripts/scan-secrets.sh +67 -0
- package/claude-toolkit/hooks/scripts/verify-clean.sh +50 -0
- package/claude-toolkit/package.json +22 -0
- package/claude-toolkit/plugin.json +31 -0
- package/claude-toolkit/skills/bfsi-api-endpoint/SKILL.md +105 -0
- package/claude-toolkit/skills/bfsi-commit/SKILL.md +102 -0
- package/claude-toolkit/skills/bfsi-compliance-check/SKILL.md +107 -0
- package/claude-toolkit/skills/bfsi-encrypt-helper/SKILL.md +127 -0
- package/claude-toolkit/skills/bfsi-error-message/SKILL.md +162 -0
- package/claude-toolkit/skills/bfsi-feature/SKILL.md +120 -0
- package/claude-toolkit/skills/bfsi-feature/references/architecture.md +69 -0
- package/claude-toolkit/skills/bfsi-feature/references/audit-events.md +70 -0
- package/claude-toolkit/skills/bfsi-feature/scripts/scaffold.mjs +136 -0
- package/claude-toolkit/skills/bfsi-form/SKILL.md +73 -0
- package/claude-toolkit/skills/bfsi-form/references/validation-regex.md +50 -0
- package/claude-toolkit/skills/bfsi-onboarding/SKILL.md +110 -0
- package/claude-toolkit/skills/bfsi-pii-field/SKILL.md +90 -0
- package/claude-toolkit/skills/bfsi-test-pattern/SKILL.md +179 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +339 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/templates/_shared/.claude/settings.json +31 -0
- package/templates/_shared/.env.local.sample +25 -0
- package/templates/_shared/.github/workflows/ci.yml +49 -0
- package/templates/_shared/CLAUDE.md +89 -0
- package/templates/_shared/README.md +50 -0
- package/templates/_shared/index.html +16 -0
- package/templates/_shared/package.json +73 -0
- package/templates/_shared/postcss.config.cjs +6 -0
- package/templates/_shared/src/app/App.tsx +13 -0
- package/templates/_shared/src/app/globals.css +64 -0
- package/templates/_shared/src/env.ts +33 -0
- package/templates/_shared/src/i18n/i18n.ts +18 -0
- package/templates/_shared/src/i18n/translations/en.json +54 -0
- package/templates/_shared/src/i18n/translations/hi.json +30 -0
- package/templates/_shared/src/main.tsx +16 -0
- package/templates/_shared/src/routes/ProtectedRoute.tsx +28 -0
- package/templates/_shared/src/routes/index.tsx +67 -0
- package/templates/_shared/src/shared/ErrorBoundary.tsx +60 -0
- package/templates/_shared/tailwind.config.ts +68 -0
- package/templates/_shared/tests/setup.ts +7 -0
- package/templates/_shared/tsconfig.json +33 -0
- package/templates/_shared/tsconfig.node.json +13 -0
- package/templates/_shared/vite.config.ts +47 -0
- package/templates/rtk-query/.claude/skills/axios-auth/SKILL.md +103 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/error-shape.md +84 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +146 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/notification-wiring.md +141 -0
- package/templates/rtk-query/.claude/skills/constants-organization/SKILL.md +112 -0
- package/templates/rtk-query/.claude/skills/constants-organization/references/example-files.md +134 -0
- package/templates/rtk-query/.claude/skills/constants-organization/references/tag-types-catalog.md +53 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/SKILL.md +159 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/references/localStorage-persistence.md +70 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/references/middleware-patterns.md +82 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/SKILL.md +148 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/cache-strategies.md +96 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/endpoint-cookbook.md +145 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/optimistic-update.md +53 -0
- package/templates/rtk-query/README.md +84 -0
- package/templates/rtk-query/package.partial.json +7 -0
- package/templates/rtk-query/src/app/App.tsx +23 -0
- package/templates/rtk-query/src/axiosconfig/axiosInstance.ts +26 -0
- package/templates/rtk-query/src/axiosconfig/baseQuery.ts +72 -0
- package/templates/rtk-query/src/axiosconfig/interceptor.ts +42 -0
- package/templates/rtk-query/src/redux/invalidateCacheMiddleware.ts +20 -0
- package/templates/rtk-query/src/redux/reduxHooks.ts +10 -0
- package/templates/rtk-query/src/redux/rootReducer.ts +18 -0
- package/templates/rtk-query/src/redux/store.ts +36 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/SKILL.md +109 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/error-shape.md +89 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +121 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/notification-pattern.md +109 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/SKILL.md +144 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/references/example-files.md +111 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/references/query-key-factories.md +129 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/SKILL.md +165 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/references/devtools.md +67 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/references/global-handlers.md +94 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/SKILL.md +142 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/audited-mutation.md +144 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/optimistic-update.md +102 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/service-cookbook.md +151 -0
- package/templates/tanstack-query/README.md +63 -0
- package/templates/tanstack-query/package.partial.json +8 -0
- package/templates/tanstack-query/src/api/axiosInstance.ts +20 -0
- package/templates/tanstack-query/src/api/http.ts +62 -0
- package/templates/tanstack-query/src/api/queryClient.ts +28 -0
- package/templates/tanstack-query/src/app/App.tsx +20 -0
- package/templates/tanstack-query/src/services/example.ts +32 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# queryKey factory pattern
|
|
2
|
+
|
|
3
|
+
QueryKeys are TanStack Query's primary cache identifier. They MUST be:
|
|
4
|
+
|
|
5
|
+
- Stable: same logical query → same key shape
|
|
6
|
+
- Discoverable: cache invalidation should be easy to write
|
|
7
|
+
- Type-safe: keys derived from a factory, not hand-typed
|
|
8
|
+
|
|
9
|
+
## Anti-pattern: inline arrays
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// ❌ Don't do this:
|
|
13
|
+
useQuery({ queryKey: ['kyc', id], queryFn: () => getKyc(id) });
|
|
14
|
+
|
|
15
|
+
// ❌ Or this:
|
|
16
|
+
queryClient.invalidateQueries({ queryKey: ['kyc'] });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Problems:
|
|
20
|
+
|
|
21
|
+
- Renaming the feature requires grep across the whole codebase
|
|
22
|
+
- Easy to typo the queryKey, causing silent cache misses
|
|
23
|
+
- Filter shapes embedded in keys aren't reused
|
|
24
|
+
|
|
25
|
+
## Pattern: factory objects
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// src/utils/constants/queryKeys.ts
|
|
29
|
+
export const kycKeys = {
|
|
30
|
+
all: ['kyc'] as const,
|
|
31
|
+
lists: () => [...kycKeys.all, 'list'] as const,
|
|
32
|
+
list: (filters?: KycFilters) => [...kycKeys.lists(), filters] as const,
|
|
33
|
+
details: () => [...kycKeys.all, 'detail'] as const,
|
|
34
|
+
detail: (id: string) => [...kycKeys.details(), id] as const,
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
useQuery({
|
|
42
|
+
queryKey: kycKeys.detail(id),
|
|
43
|
+
queryFn: () => getKyc(id),
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Invalidation matrix
|
|
48
|
+
|
|
49
|
+
| Goal | Code |
|
|
50
|
+
| ------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
51
|
+
| Invalidate everything for the feature | `queryClient.invalidateQueries({ queryKey: kycKeys.all })` |
|
|
52
|
+
| Invalidate all lists (any filter) | `queryClient.invalidateQueries({ queryKey: kycKeys.lists() })` |
|
|
53
|
+
| Invalidate one specific list | `queryClient.invalidateQueries({ queryKey: kycKeys.list({ status: 'pending' }) })` |
|
|
54
|
+
| Invalidate all details | `queryClient.invalidateQueries({ queryKey: kycKeys.details() })` |
|
|
55
|
+
| Invalidate one record | `queryClient.invalidateQueries({ queryKey: kycKeys.detail(id) })` |
|
|
56
|
+
|
|
57
|
+
TanStack Query matches by **prefix** — `kycKeys.all` invalidates everything that starts with `['kyc']`.
|
|
58
|
+
|
|
59
|
+
## Pattern: after a mutation
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
const queryClient = useQueryClient();
|
|
63
|
+
|
|
64
|
+
const submit = useMutation({
|
|
65
|
+
mutationFn: submitKyc,
|
|
66
|
+
onSuccess: (newRecord) => {
|
|
67
|
+
// Drop the lists cache so the next list query refetches
|
|
68
|
+
queryClient.invalidateQueries({ queryKey: kycKeys.lists() });
|
|
69
|
+
// Optionally pre-fill the detail cache so navigating to it is instant
|
|
70
|
+
queryClient.setQueryData(kycKeys.detail(newRecord.id), newRecord);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Pattern: cross-feature invalidation
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
const submit = useMutation({
|
|
79
|
+
mutationFn: submitKyc,
|
|
80
|
+
onSuccess: () => {
|
|
81
|
+
queryClient.invalidateQueries({ queryKey: kycKeys.all });
|
|
82
|
+
queryClient.invalidateQueries({ queryKey: userKeys.detail(userId) }); // KYC changes affect user profile
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For complex graphs, consider a small middleware-like wrapper:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// src/services/invalidations.ts
|
|
91
|
+
export const invalidationsAfter = {
|
|
92
|
+
kycSubmit: (qc: QueryClient, userId: string) => {
|
|
93
|
+
qc.invalidateQueries({ queryKey: kycKeys.all });
|
|
94
|
+
qc.invalidateQueries({ queryKey: userKeys.detail(userId) });
|
|
95
|
+
},
|
|
96
|
+
// ... other cross-feature invalidation recipes
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Then in components:
|
|
100
|
+
const submit = useMutation({
|
|
101
|
+
mutationFn: submitKyc,
|
|
102
|
+
onSuccess: () => invalidationsAfter.kycSubmit(queryClient, currentUserId),
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Anti-pattern: keys in the wrong order
|
|
107
|
+
|
|
108
|
+
The order matters — TanStack Query treats the array as a prefix. Always put the most general element first:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// ✅ Good
|
|
112
|
+
list: (filters) => ['kyc', 'list', filters] as const;
|
|
113
|
+
|
|
114
|
+
// ❌ Bad — can't invalidate "all kyc" without listing every filter
|
|
115
|
+
list: (filters) => [filters, 'kyc', 'list'] as const;
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Logout: nuke the cache
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { kycKeys } from '@/utils/constants/queryKeys';
|
|
122
|
+
|
|
123
|
+
function logout() {
|
|
124
|
+
// ... clear auth
|
|
125
|
+
queryClient.clear(); // wipes EVERYTHING (preferred on logout)
|
|
126
|
+
// alternative — selective:
|
|
127
|
+
// queryClient.removeQueries({ queryKey: kycKeys.all });
|
|
128
|
+
}
|
|
129
|
+
```
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: query-client-setup
|
|
3
|
+
description: Configure the TanStack QueryClient — defaults, retry policy, refetch behaviour, optional global error handlers, devtools. Use when setting up the QueryClient for the first time, adjusting retry / staleTime / refetch behaviour, adding a global error handler, or wiring devtools.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# QueryClient Setup
|
|
7
|
+
|
|
8
|
+
`src/api/queryClient.ts` exports a single `QueryClient` instance with BFSI-friendly defaults. It's mounted via `<QueryClientProvider>` in `src/app/App.tsx`.
|
|
9
|
+
|
|
10
|
+
## File map
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
src/api/
|
|
14
|
+
├── axiosInstance.ts single axios for all services
|
|
15
|
+
├── http.ts typed GET/POST/PUT/PATCH/DELETE helpers
|
|
16
|
+
└── queryClient.ts QueryClient with defaultOptions
|
|
17
|
+
src/app/
|
|
18
|
+
└── App.tsx wraps in <QueryClientProvider client={queryClient}>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Default config (what the scaffolder ships)
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
25
|
+
|
|
26
|
+
export const queryClient = new QueryClient({
|
|
27
|
+
defaultOptions: {
|
|
28
|
+
queries: {
|
|
29
|
+
staleTime: 30_000, // 30s — minimise thrash, refetch when stale
|
|
30
|
+
gcTime: 5 * 60_000, // 5min — keep in cache after unsubscribed
|
|
31
|
+
retry: (failureCount, error) => {
|
|
32
|
+
const status = (error as { status?: number })?.status;
|
|
33
|
+
// Don't retry 4xx (except 408/429)
|
|
34
|
+
if (status && status >= 400 && status < 500 && status !== 408 && status !== 429) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return failureCount < 2;
|
|
38
|
+
},
|
|
39
|
+
refetchOnWindowFocus: false, // BFSI: don't refetch on tab switch
|
|
40
|
+
},
|
|
41
|
+
mutations: {
|
|
42
|
+
retry: false, // Never auto-retry mutations
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Why these defaults
|
|
49
|
+
|
|
50
|
+
| Setting | Choice | Reason |
|
|
51
|
+
| ----------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `staleTime: 30_000` | 30 seconds | Prevents thrash from rapid component remounts; long enough that two users on the same screen don't both hit the API |
|
|
53
|
+
| `gcTime: 5 * 60_000` | 5 minutes | Keeps caches around when user navigates back; cleared on logout via `queryClient.clear()` |
|
|
54
|
+
| `retry: 4xx → no` | 4xx → no, 5xx → 2 | 4xx is a contract issue — retrying won't help. 5xx might be transient. |
|
|
55
|
+
| `refetchOnWindowFocus: false` | off | BFSI users tab away and back; surprise refetches lose draft state |
|
|
56
|
+
| `mutations.retry: false` | never retry | Mutations have side effects. Use idempotency-key + explicit user retry. |
|
|
57
|
+
|
|
58
|
+
## Workflow — adjusting defaults
|
|
59
|
+
|
|
60
|
+
Don't override these in component-level `useQuery` calls unless you have a specific reason. Override the default once in `queryClient.ts`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
defaultOptions: {
|
|
64
|
+
queries: {
|
|
65
|
+
staleTime: 60_000, // change to 60s
|
|
66
|
+
// ...
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For a per-feature override:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
const { data } = useQuery({
|
|
75
|
+
queryKey: kycKeys.list(filters),
|
|
76
|
+
queryFn: () => getKycList(filters),
|
|
77
|
+
staleTime: 0, // always refetch on mount (audit-critical list)
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Wiring into the app
|
|
82
|
+
|
|
83
|
+
`src/app/App.tsx` (variant overlay):
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
87
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
88
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
89
|
+
import { ErrorBoundary } from '../shared/ErrorBoundary';
|
|
90
|
+
import { AppRoutes } from '../routes';
|
|
91
|
+
import { queryClient } from '../api/queryClient';
|
|
92
|
+
|
|
93
|
+
export function App(): JSX.Element {
|
|
94
|
+
return (
|
|
95
|
+
<ErrorBoundary>
|
|
96
|
+
<QueryClientProvider client={queryClient}>
|
|
97
|
+
<BrowserRouter>
|
|
98
|
+
<AppRoutes />
|
|
99
|
+
</BrowserRouter>
|
|
100
|
+
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
|
101
|
+
</QueryClientProvider>
|
|
102
|
+
</ErrorBoundary>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Devtools render only in dev (gated by `import.meta.env.DEV`).
|
|
108
|
+
|
|
109
|
+
## Global error handlers (optional)
|
|
110
|
+
|
|
111
|
+
For a "catch all unhandled mutation errors" toast, use a `MutationCache`:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { QueryClient, MutationCache } from '@tanstack/react-query';
|
|
115
|
+
import { toast } from 'sonner';
|
|
116
|
+
import { toSafeView } from '@<scope>/core/compliance';
|
|
117
|
+
import type { ApiError } from '@<scope>/core/http';
|
|
118
|
+
import i18n from '@/i18n/i18n';
|
|
119
|
+
|
|
120
|
+
export const queryClient = new QueryClient({
|
|
121
|
+
mutationCache: new MutationCache({
|
|
122
|
+
onError: (error, _vars, _ctx, mutation) => {
|
|
123
|
+
if (mutation.options.onError) return; // skip if per-mutation handler set
|
|
124
|
+
const view = toSafeView(error as ApiError, i18n.t);
|
|
125
|
+
toast.error(view.title, { description: view.description });
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
defaultOptions: {
|
|
129
|
+
/* ... */
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
See [`references/global-handlers.md`](references/global-handlers.md) for QueryCache + MutationCache patterns.
|
|
135
|
+
|
|
136
|
+
## Conventions enforced
|
|
137
|
+
|
|
138
|
+
- ❌ NEVER create multiple `QueryClient` instances — there must be exactly one for the app.
|
|
139
|
+
- ❌ NEVER set `refetchOnWindowFocus: true` globally (BFSI default is off).
|
|
140
|
+
- ❌ NEVER enable `mutations.retry > 0` globally — mutations have side effects.
|
|
141
|
+
- ❌ NEVER render `<ReactQueryDevtools>` in production builds — gate on `import.meta.env.DEV`.
|
|
142
|
+
- ✅ `<QueryClientProvider>` wraps the WHOLE app (above the router, below ErrorBoundary).
|
|
143
|
+
- ✅ Per-query overrides go in the `useQuery` call, not the QueryClient config.
|
|
144
|
+
- ✅ On logout: `queryClient.clear()` after `clearAuthToken()`.
|
|
145
|
+
|
|
146
|
+
## Logout sequence
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { clearAuthToken } from '@<scope>/core/http';
|
|
150
|
+
import { queryClient } from '@/api/queryClient';
|
|
151
|
+
import axiosInstance from '@/api/axiosInstance';
|
|
152
|
+
|
|
153
|
+
export function logout() {
|
|
154
|
+
clearAuthToken(axiosInstance); // drop the auth header
|
|
155
|
+
queryClient.clear(); // wipe all cached server data
|
|
156
|
+
navigate('/login');
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`queryClient.clear()` removes all cached queries + in-flight fetches. Without it, a re-login on the same browser sees stale data for a flash before refetch.
|
|
161
|
+
|
|
162
|
+
## References
|
|
163
|
+
|
|
164
|
+
- [`references/global-handlers.md`](references/global-handlers.md) — QueryCache.onError + MutationCache.onError patterns
|
|
165
|
+
- [`references/devtools.md`](references/devtools.md) — using ReactQueryDevtools effectively
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Using ReactQueryDevtools effectively
|
|
2
|
+
|
|
3
|
+
The devtools panel is the single most useful tool for debugging TanStack Query behaviour. Render it conditionally so it never reaches production bundles.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
9
|
+
|
|
10
|
+
<QueryClientProvider client={queryClient}>
|
|
11
|
+
{/* app */}
|
|
12
|
+
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
|
|
13
|
+
</QueryClientProvider>;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`@tanstack/react-query-devtools` is in `devDependencies` and Vite tree-shakes it out of the production bundle as long as the import is gated behind `import.meta.env.DEV`. Confirm with `pnpm build && grep -r 'ReactQueryDevtools' dist/`.
|
|
17
|
+
|
|
18
|
+
## Debugging workflow
|
|
19
|
+
|
|
20
|
+
When something feels wrong with caching/refetching:
|
|
21
|
+
|
|
22
|
+
1. **Open devtools** (icon in bottom-right corner).
|
|
23
|
+
2. **Find the queryKey** — search the list for your feature (e.g. `kyc`).
|
|
24
|
+
3. **Inspect the state**:
|
|
25
|
+
- `fetchStatus: idle/fetching/paused` — is it actually fetching?
|
|
26
|
+
- `status: pending/success/error` — what's the last result?
|
|
27
|
+
- `data` — the parsed response (or `undefined`)
|
|
28
|
+
- `error` — the ApiError instance
|
|
29
|
+
4. **Check observers** — which components are subscribed? If 0, the query won't refetch on stale.
|
|
30
|
+
5. **Manual invalidate** — click "Invalidate" to force-refetch and see if the issue is invalidation logic vs the network call.
|
|
31
|
+
6. **Time travel** — switch query state to "Fresh" or "Stale" to test edge cases.
|
|
32
|
+
|
|
33
|
+
## Common issues + how the devtools reveal them
|
|
34
|
+
|
|
35
|
+
| Symptom | What devtools shows | Fix |
|
|
36
|
+
| ------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------- |
|
|
37
|
+
| List doesn't refetch after a mutation | List query is "Fresh" not "Stale" after the mutation | Missing `invalidateQueries` in mutation's `onSuccess` |
|
|
38
|
+
| Query won't fire | `fetchStatus: paused`, `enabled: false` | Some upstream variable is undefined; check the `enabled:` flag |
|
|
39
|
+
| Two components show different data | They have different queryKey shapes | Normalise queryKey via a factory |
|
|
40
|
+
| Cache size grows forever | Many entries with `observers: 0` | `gcTime` too long; lower it |
|
|
41
|
+
| Mutation seems to retry | Multiple "pending" entries | Check `mutations.retry` is `false` |
|
|
42
|
+
|
|
43
|
+
## Don't ship devtools to prod
|
|
44
|
+
|
|
45
|
+
Risk: devtools expose query payloads, including PII. Even though they only render `import.meta.env.DEV`, double-check:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pnpm build
|
|
49
|
+
grep -r 'ReactQueryDevtools' dist/ # should be empty
|
|
50
|
+
grep -r 'TanStack Query' dist/ # should be empty
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If you see hits, the gating is wrong — ReactQueryDevtools is in the production bundle.
|
|
54
|
+
|
|
55
|
+
## Alternative for staging
|
|
56
|
+
|
|
57
|
+
If you need devtools-like inspection in staging (not full prod), add a feature flag:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
{
|
|
61
|
+
(import.meta.env.DEV || env.VITE_ENABLE_QUERY_DEVTOOLS === 'true') && (
|
|
62
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
NEVER set `VITE_ENABLE_QUERY_DEVTOOLS=true` in production — it ships the devtools.
|
package/templates/tanstack-query/.claude/skills/query-client-setup/references/global-handlers.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Global QueryCache / MutationCache handlers
|
|
2
|
+
|
|
3
|
+
For cross-cutting concerns (every-error toast, every-success audit), use the cache-level callbacks instead of repeating `onError` in every component.
|
|
4
|
+
|
|
5
|
+
## When to use cache-level vs per-call
|
|
6
|
+
|
|
7
|
+
| Concern | Where | Why |
|
|
8
|
+
| -------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------- |
|
|
9
|
+
| Default error toast | `MutationCache.onError` | DRY — every component without its own onError gets it |
|
|
10
|
+
| Per-mutation audit event | `useAuditedMutation` wrapper | Audit needs event name, target, etc. — too granular for cache-level |
|
|
11
|
+
| Global "session expired" handler | `axios.onUnauthorized` (in `createAxios`) | 401 is handled before the error reaches React; no cache callback needed |
|
|
12
|
+
| Logging all query failures | `QueryCache.onError` | Observability — fire-and-forget telemetry |
|
|
13
|
+
| Per-query field error mapping | Per-mutation `onError` | Field errors are component-specific (which RHF form to set them on) |
|
|
14
|
+
|
|
15
|
+
## MutationCache pattern
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { QueryClient, MutationCache } from '@tanstack/react-query';
|
|
19
|
+
import { toast } from 'sonner';
|
|
20
|
+
import { toSafeView } from '@<scope>/core/compliance';
|
|
21
|
+
import type { ApiError } from '@<scope>/core/http';
|
|
22
|
+
import i18n from '@/i18n/i18n';
|
|
23
|
+
|
|
24
|
+
export const queryClient = new QueryClient({
|
|
25
|
+
mutationCache: new MutationCache({
|
|
26
|
+
onError: (error, _vars, _ctx, mutation) => {
|
|
27
|
+
// Skip if mutation defined its own onError
|
|
28
|
+
if (mutation.options.onError) return;
|
|
29
|
+
|
|
30
|
+
const apiErr = error as ApiError;
|
|
31
|
+
// Skip 401 (handled by axios.onUnauthorized)
|
|
32
|
+
if (apiErr.kind === 'unauthorized') return;
|
|
33
|
+
|
|
34
|
+
const view = toSafeView(apiErr, i18n.t);
|
|
35
|
+
toast.error(view.title, { description: view.description });
|
|
36
|
+
},
|
|
37
|
+
onSuccess: (_data, _vars, _ctx, mutation) => {
|
|
38
|
+
const successMessage = (mutation.options as { successMessage?: string }).successMessage;
|
|
39
|
+
if (successMessage) toast.success(successMessage);
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
// ... defaultOptions
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Note: `mutation.options.onError` being set means the component has opted-in to handle the error itself. Skip the global toast to avoid double-toasting.
|
|
47
|
+
|
|
48
|
+
## QueryCache pattern (rarer)
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { QueryCache } from '@tanstack/react-query';
|
|
52
|
+
import * as Sentry from '@sentry/react';
|
|
53
|
+
|
|
54
|
+
queryCache: new QueryCache({
|
|
55
|
+
onError: (error, query) => {
|
|
56
|
+
// Don't toast on query errors (they may fire in background)
|
|
57
|
+
// Just log to telemetry
|
|
58
|
+
Sentry.captureException(error, {
|
|
59
|
+
tags: { queryKey: JSON.stringify(query.queryKey) },
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Don't surface query errors to UI via global toast — they fire in background and may be for unmounted components. Let the component handle its own error state via `{ error, isError } = useQuery(...)`.
|
|
66
|
+
|
|
67
|
+
## Anti-patterns
|
|
68
|
+
|
|
69
|
+
❌ `MutationCache.onError` that always fires even when the component has its own:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// WRONG — double-toasts
|
|
73
|
+
onError: (error) => {
|
|
74
|
+
toast.error(safeMessage(error)); // fires even if component handled it
|
|
75
|
+
},
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
❌ `QueryCache.onError` that toasts:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// WRONG — toasts for background refetches the user didn't trigger
|
|
82
|
+
onError: (error) => toast.error(safeMessage(error)),
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
❌ Per-mutation `onError` that re-throws (TanStack catches and routes via cache):
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// WRONG — onError shouldn't throw
|
|
89
|
+
useMutation({
|
|
90
|
+
onError: (err) => {
|
|
91
|
+
throw err;
|
|
92
|
+
}, // breaks rollback chain
|
|
93
|
+
});
|
|
94
|
+
```
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-services
|
|
3
|
+
description: Create the service layer for a feature — typed async functions that wrap the HTTP helpers (GET/POST/PUT/PATCH/DELETE) and return parsed responses. Services are hook-free and consumed by useQuery / useMutation in components. Use when adding a new feature's services, adding a new service to an existing feature, or wiring API endpoints.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TanStack Services
|
|
7
|
+
|
|
8
|
+
Every feature has a service file at `src/services/<feature>.ts` (or `src/features/<Feature>/service.ts`). Services are plain async functions: typed inputs in, typed parsed output out. No hooks. No React.
|
|
9
|
+
|
|
10
|
+
## File map (typical feature)
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
src/features/<Feature>/
|
|
14
|
+
├── service.ts typed async functions
|
|
15
|
+
├── schema.ts Zod schemas (request + response)
|
|
16
|
+
├── types.ts inferred types from Zod
|
|
17
|
+
├── containers/... React components that call services via useQuery/useMutation
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Workflow — new feature service
|
|
21
|
+
|
|
22
|
+
### Step 1 — Define schemas
|
|
23
|
+
|
|
24
|
+
`src/features/Kyc/schema.ts`:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { z } from 'zod';
|
|
28
|
+
import { PAN_REGEX, AADHAAR_REGEX } from '@/utils/constants/regexConstants';
|
|
29
|
+
|
|
30
|
+
export const kycRecordSchema = z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
pan: z.string().regex(PAN_REGEX),
|
|
33
|
+
aadhaar: z.string().regex(AADHAAR_REGEX),
|
|
34
|
+
status: z.enum(['pending', 'approved', 'rejected']),
|
|
35
|
+
createdAt: z.coerce.date(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const kycListResponseSchema = z.object({
|
|
39
|
+
items: z.array(kycRecordSchema),
|
|
40
|
+
total: z.number().int().nonnegative(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const kycSubmitRequestSchema = z.object({
|
|
44
|
+
pan: z.string().regex(PAN_REGEX),
|
|
45
|
+
aadhaar: z.string().regex(AADHAAR_REGEX),
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Types are inferred — never hand-write:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// types.ts
|
|
53
|
+
import { z } from 'zod';
|
|
54
|
+
import type { kycRecordSchema, kycListResponseSchema, kycSubmitRequestSchema } from './schema';
|
|
55
|
+
|
|
56
|
+
export type IKycRecord = z.infer<typeof kycRecordSchema>;
|
|
57
|
+
export type IKycListResponse = z.infer<typeof kycListResponseSchema>;
|
|
58
|
+
export type IKycSubmitRequest = z.infer<typeof kycSubmitRequestSchema>;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Step 2 — Write services
|
|
62
|
+
|
|
63
|
+
`src/features/Kyc/service.ts`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { GET, POST } from '@/api/http';
|
|
67
|
+
import { KYC_URLS } from '@/utils/constants/urlConstants';
|
|
68
|
+
import { kycRecordSchema, kycListResponseSchema } from './schema';
|
|
69
|
+
import type { IKycRecord, IKycListResponse, IKycSubmitRequest } from './types';
|
|
70
|
+
|
|
71
|
+
export const getKycList = async (): Promise<IKycListResponse> => {
|
|
72
|
+
const raw = await GET<unknown>(KYC_URLS.LIST);
|
|
73
|
+
return kycListResponseSchema.parse(raw);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const getKycDetail = async (id: string): Promise<IKycRecord> => {
|
|
77
|
+
const raw = await GET<unknown>(KYC_URLS.DETAIL(id));
|
|
78
|
+
return kycRecordSchema.parse(raw);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const submitKyc = async (payload: IKycSubmitRequest): Promise<IKycRecord> => {
|
|
82
|
+
const raw = await POST<unknown, IKycSubmitRequest>(KYC_URLS.SUBMIT, payload);
|
|
83
|
+
return kycRecordSchema.parse(raw);
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Step 3 — Use in components
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
91
|
+
import { kycKeys } from '@/utils/constants/queryKeys';
|
|
92
|
+
import { getKycList, getKycDetail, submitKyc } from './service';
|
|
93
|
+
|
|
94
|
+
function KycList() {
|
|
95
|
+
const { data, isLoading, error } = useQuery({
|
|
96
|
+
queryKey: kycKeys.lists(),
|
|
97
|
+
queryFn: getKycList,
|
|
98
|
+
});
|
|
99
|
+
// ...
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function KycForm() {
|
|
103
|
+
const queryClient = useQueryClient();
|
|
104
|
+
const submit = useMutation({
|
|
105
|
+
mutationFn: submitKyc,
|
|
106
|
+
onSuccess: () => {
|
|
107
|
+
queryClient.invalidateQueries({ queryKey: kycKeys.all });
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
// ...
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Conventions enforced
|
|
115
|
+
|
|
116
|
+
- ❌ NEVER use `axios.<method>` directly — use the typed `GET<TRes, TParams>` / `POST<TRes, TReq>` helpers from `@/api/http`.
|
|
117
|
+
- ❌ NEVER skip the `.parse()` step — every response goes through Zod validation at runtime.
|
|
118
|
+
- ❌ NEVER inline a URL string — use `urlConstants.ts`.
|
|
119
|
+
- ❌ NEVER call services without typing the response — the generic on `GET<IResponse>` is required.
|
|
120
|
+
- ❌ NEVER put React hooks in service files — services are pure async functions.
|
|
121
|
+
- ✅ One service file per feature; export named async functions.
|
|
122
|
+
- ✅ Request types prefixed `I` (`IKycSubmitRequest`) to match service interface convention.
|
|
123
|
+
- ✅ Responses parsed by Zod schemas from the same feature's `schema.ts`.
|
|
124
|
+
- ✅ Use `getX` / `submitX` / `updateX` / `deleteX` naming for service functions.
|
|
125
|
+
|
|
126
|
+
## Service vs hook
|
|
127
|
+
|
|
128
|
+
A service is a plain function: `getKyc(id): Promise<IKycRecord>`.
|
|
129
|
+
|
|
130
|
+
A hook wraps it with TanStack Query: `useQuery({ queryKey: kycKeys.detail(id), queryFn: () => getKyc(id) })`.
|
|
131
|
+
|
|
132
|
+
Don't pre-bundle the hook into the service. Keep services hook-free so:
|
|
133
|
+
|
|
134
|
+
- They're trivial to unit test (call them directly with mocked axios)
|
|
135
|
+
- The component decides cache behaviour (staleTime, refetch, enabled, etc.)
|
|
136
|
+
- The same service can power useQuery, useMutation, OR a direct call outside a component (rare but useful — e.g. in a logout flow)
|
|
137
|
+
|
|
138
|
+
## References
|
|
139
|
+
|
|
140
|
+
- [`references/service-cookbook.md`](references/service-cookbook.md) — list, detail, paginated, file-upload, file-download, polling examples
|
|
141
|
+
- [`references/optimistic-update.md`](references/optimistic-update.md) — optimistic mutations with cache patches
|
|
142
|
+
- [`references/audited-mutation.md`](references/audited-mutation.md) — wrapping useMutation to fire audit events
|