@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,96 @@
|
|
|
1
|
+
# Cache strategies — when to use which tagging pattern
|
|
2
|
+
|
|
3
|
+
## Single-entity tag
|
|
4
|
+
|
|
5
|
+
When a feature has one type of resource:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
tagTypes: ['Kyc'];
|
|
9
|
+
// queries: providesTags: ['Kyc']
|
|
10
|
+
// mutations: invalidatesTags: ['Kyc']
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Simple. Every mutation refetches everything tagged `Kyc`. Fine for small lists.
|
|
14
|
+
|
|
15
|
+
## Per-record tags (id-scoped)
|
|
16
|
+
|
|
17
|
+
When the list is large and you want detail updates to NOT refetch the whole list:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
providesTags: (result) =>
|
|
21
|
+
result
|
|
22
|
+
? [
|
|
23
|
+
...result.items.map(({ id }) => ({ type: 'Kyc' as const, id })),
|
|
24
|
+
{ type: 'Kyc' as const, id: 'LIST' },
|
|
25
|
+
]
|
|
26
|
+
: [{ type: 'Kyc' as const, id: 'LIST' }],
|
|
27
|
+
|
|
28
|
+
// Detail provides one tag:
|
|
29
|
+
providesTags: (_, __, id) => [{ type: 'Kyc', id }],
|
|
30
|
+
|
|
31
|
+
// Update mutation invalidates that one tag:
|
|
32
|
+
invalidatesTags: (_, __, { id }) => [{ type: 'Kyc', id }],
|
|
33
|
+
|
|
34
|
+
// Create mutation invalidates only the list:
|
|
35
|
+
invalidatesTags: [{ type: 'Kyc', id: 'LIST' }],
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The `LIST` is a synthetic id used as the "any list" tag.
|
|
39
|
+
|
|
40
|
+
## Tag everything mutating affects (mutation joined with list)
|
|
41
|
+
|
|
42
|
+
When an update changes a derived list (e.g. updating a transaction affects account balance):
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
updateTransaction: builder.mutation({
|
|
46
|
+
invalidatesTags: (_, __, { id }) => [
|
|
47
|
+
{ type: 'Transaction', id },
|
|
48
|
+
{ type: 'AccountBalance' }, // tag from a different api
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For cross-API invalidations, the target API must also be in the `tagTypes` registry or you need `invalidateCacheMiddleware` instead.
|
|
54
|
+
|
|
55
|
+
## Stale-while-revalidate
|
|
56
|
+
|
|
57
|
+
Default RTK Query behaviour — cached data shows immediately, refetch happens in background. To control:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
getKycList: builder.query({
|
|
61
|
+
query: ...,
|
|
62
|
+
keepUnusedDataFor: 60, // seconds after last subscriber unsubscribes
|
|
63
|
+
refetchOnMountOrArgChange: 30, // seconds — refetch if cache > 30s old
|
|
64
|
+
}),
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For audit-critical lists where staleness is unacceptable, set `keepUnusedDataFor: 0` so cache is wiped on unmount.
|
|
68
|
+
|
|
69
|
+
## Skip cache (force fresh)
|
|
70
|
+
|
|
71
|
+
Pass `{ refetchOnMountOrArgChange: true }` to a single hook call:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
const { data } = useGetKycListQuery(undefined, { refetchOnMountOrArgChange: true });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use ONLY for sensitive views (audit log, balance display in transaction flow).
|
|
78
|
+
|
|
79
|
+
## Pagination + cache
|
|
80
|
+
|
|
81
|
+
When paginating, key the query by page so each page caches independently:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
getKycList: builder.query<KycListResponse, { page: number; pageSize: number }>({
|
|
85
|
+
query: (params) => ({ url: KYC_URLS.LIST, method: GET, data: params }),
|
|
86
|
+
providesTags: (result, _, arg) =>
|
|
87
|
+
result
|
|
88
|
+
? [
|
|
89
|
+
...result.items.map(({ id }) => ({ type: 'Kyc' as const, id })),
|
|
90
|
+
{ type: 'Kyc' as const, id: `PAGE-${arg.page}` },
|
|
91
|
+
]
|
|
92
|
+
: [{ type: 'Kyc' as const, id: `PAGE-${arg.page}` }],
|
|
93
|
+
}),
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This way refetching one page doesn't blow away the others.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Endpoint cookbook
|
|
2
|
+
|
|
3
|
+
Templates for the common shapes. Copy + adapt.
|
|
4
|
+
|
|
5
|
+
## List
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
getKycList: builder.query<KycListResponse, void>({
|
|
9
|
+
query: () => ({ url: KYC_URLS.LIST, method: GET }),
|
|
10
|
+
transformResponse: (raw: unknown) => kycListResponseSchema.parse(raw),
|
|
11
|
+
providesTags: ['Kyc'],
|
|
12
|
+
}),
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Paginated list
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
getKycList: builder.query<KycListResponse, { page: number; pageSize: number; status?: KycStatus }>({
|
|
19
|
+
query: (params) => ({ url: KYC_URLS.LIST, method: GET, data: params }),
|
|
20
|
+
transformResponse: (raw: unknown) => kycListResponseSchema.parse(raw),
|
|
21
|
+
providesTags: (result) =>
|
|
22
|
+
result
|
|
23
|
+
? [
|
|
24
|
+
...result.items.map(({ id }) => ({ type: 'Kyc' as const, id })),
|
|
25
|
+
{ type: 'Kyc' as const, id: 'LIST' },
|
|
26
|
+
]
|
|
27
|
+
: [{ type: 'Kyc' as const, id: 'LIST' }],
|
|
28
|
+
}),
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Detail
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
getKycDetail: builder.query<KycRecord, string>({
|
|
35
|
+
query: (id) => ({ url: KYC_URLS.DETAIL(id), method: GET }),
|
|
36
|
+
transformResponse: (raw: unknown) => kycRecordSchema.parse(raw),
|
|
37
|
+
providesTags: (_, __, id) => [{ type: 'Kyc', id }],
|
|
38
|
+
}),
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Create (mutation)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
submitKyc: builder.mutation<KycRecord, KycSubmitRequest>({
|
|
45
|
+
query: (body) => ({
|
|
46
|
+
url: KYC_URLS.SUBMIT,
|
|
47
|
+
method: POST,
|
|
48
|
+
data: body,
|
|
49
|
+
showSuccessNotification: true,
|
|
50
|
+
showFailureNotification: true,
|
|
51
|
+
}),
|
|
52
|
+
transformResponse: (raw: unknown) => kycRecordSchema.parse(raw),
|
|
53
|
+
invalidatesTags: [{ type: 'Kyc', id: 'LIST' }],
|
|
54
|
+
}),
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Update (mutation, per-record + list invalidation)
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
updateKyc: builder.mutation<KycRecord, { id: string; body: KycUpdateRequest }>({
|
|
61
|
+
query: ({ id, body }) => ({
|
|
62
|
+
url: KYC_URLS.DETAIL(id),
|
|
63
|
+
method: PUT,
|
|
64
|
+
data: body,
|
|
65
|
+
showFailureNotification: true,
|
|
66
|
+
}),
|
|
67
|
+
transformResponse: (raw: unknown) => kycRecordSchema.parse(raw),
|
|
68
|
+
invalidatesTags: (_, __, { id }) => [
|
|
69
|
+
{ type: 'Kyc', id },
|
|
70
|
+
{ type: 'Kyc', id: 'LIST' },
|
|
71
|
+
],
|
|
72
|
+
}),
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Delete
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
deleteKyc: builder.mutation<void, string>({
|
|
79
|
+
query: (id) => ({
|
|
80
|
+
url: KYC_URLS.DETAIL(id),
|
|
81
|
+
method: DELETE,
|
|
82
|
+
showSuccessNotification: true,
|
|
83
|
+
showFailureNotification: true,
|
|
84
|
+
}),
|
|
85
|
+
invalidatesTags: (_, __, id) => [
|
|
86
|
+
{ type: 'Kyc', id },
|
|
87
|
+
{ type: 'Kyc', id: 'LIST' },
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Polling (e.g. job status)
|
|
93
|
+
|
|
94
|
+
In the component:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
const { data } = useGetJobStatusQuery(jobId, {
|
|
98
|
+
pollingInterval: data?.status === 'pending' ? 3000 : 0,
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Skip polling once a terminal state is reached by setting interval to 0.
|
|
103
|
+
|
|
104
|
+
## File upload
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
uploadDocument: builder.mutation<DocumentRecord, FormData>({
|
|
108
|
+
query: (formData) => ({
|
|
109
|
+
url: DOCS_URLS.UPLOAD,
|
|
110
|
+
method: POST,
|
|
111
|
+
data: formData,
|
|
112
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
113
|
+
showFailureNotification: true,
|
|
114
|
+
}),
|
|
115
|
+
transformResponse: (raw: unknown) => documentRecordSchema.parse(raw),
|
|
116
|
+
invalidatesTags: ['Document'],
|
|
117
|
+
}),
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## File download (responseType blob)
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
downloadStatement: builder.query<Blob, string>({
|
|
124
|
+
query: (statementId) => ({
|
|
125
|
+
url: STATEMENT_URLS.DOWNLOAD(statementId),
|
|
126
|
+
method: GET,
|
|
127
|
+
responseType: 'blob',
|
|
128
|
+
}),
|
|
129
|
+
}),
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Trigger from component:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
const [trigger] = useLazyDownloadStatementQuery();
|
|
136
|
+
const handleDownload = async (id: string) => {
|
|
137
|
+
const { data: blob } = await trigger(id).unwrap();
|
|
138
|
+
const url = URL.createObjectURL(blob);
|
|
139
|
+
const a = document.createElement('a');
|
|
140
|
+
a.href = url;
|
|
141
|
+
a.download = `statement-${id}.pdf`;
|
|
142
|
+
a.click();
|
|
143
|
+
URL.revokeObjectURL(url);
|
|
144
|
+
};
|
|
145
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Optimistic updates
|
|
2
|
+
|
|
3
|
+
Use sparingly. Optimistic updates risk showing stale state to the user; only use when the success rate is very high (>99%) AND the perceived latency is meaningful.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
✅ Good fit:
|
|
8
|
+
|
|
9
|
+
- Toggling a boolean flag (favourited, archived, read/unread)
|
|
10
|
+
- Reordering a list (drag-and-drop)
|
|
11
|
+
- Inline editing of a single field with very low failure rate
|
|
12
|
+
|
|
13
|
+
❌ Bad fit:
|
|
14
|
+
|
|
15
|
+
- Financial transactions (NEVER show success before backend confirms)
|
|
16
|
+
- KYC submissions (regulatory + the value MIGHT be rejected by ML)
|
|
17
|
+
- Anything where the failure path is "value also gets rejected for other reasons we can't predict"
|
|
18
|
+
|
|
19
|
+
## Pattern — `onQueryStarted` with cache patch
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
toggleFavorite: builder.mutation<void, { id: string; isFavorite: boolean }>({
|
|
23
|
+
query: ({ id, isFavorite }) => ({
|
|
24
|
+
url: KYC_URLS.FAVORITE(id),
|
|
25
|
+
method: PATCH,
|
|
26
|
+
data: { isFavorite },
|
|
27
|
+
}),
|
|
28
|
+
async onQueryStarted({ id, isFavorite }, { dispatch, queryFulfilled }) {
|
|
29
|
+
// Optimistic: patch the cached list entry
|
|
30
|
+
const patch = dispatch(
|
|
31
|
+
kycApi.util.updateQueryData('getKycList', undefined, (draft) => {
|
|
32
|
+
const item = draft.items.find((x) => x.id === id);
|
|
33
|
+
if (item) (item as { isFavorite: boolean }).isFavorite = isFavorite;
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await queryFulfilled;
|
|
39
|
+
} catch {
|
|
40
|
+
// Rollback on failure
|
|
41
|
+
patch.undo();
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
invalidatesTags: (_, __, { id }) => [{ type: 'Kyc', id }],
|
|
45
|
+
}),
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Rules
|
|
49
|
+
|
|
50
|
+
1. ALWAYS roll back on failure (`patch.undo()`)
|
|
51
|
+
2. Still invalidate tags so the server's truth eventually wins
|
|
52
|
+
3. Don't show success toast optimistically — wait for `queryFulfilled`
|
|
53
|
+
4. Don't optimistically create new entries — wait for the server's ID assignment
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Template: RTK Query variant
|
|
2
|
+
|
|
3
|
+
Overlay applied on top of `templates/_shared/` when the user picks **RTK Query**.
|
|
4
|
+
|
|
5
|
+
## Structure (mirrors rsense-react-org)
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
├── axiosconfig/
|
|
10
|
+
│ ├── axiosInstance.ts # single shared axios instance
|
|
11
|
+
│ ├── interceptor.ts # response interceptor (notifications, 401)
|
|
12
|
+
│ └── baseQuery.ts # axiosBaseQuery for RTK Query
|
|
13
|
+
├── redux/
|
|
14
|
+
│ ├── store.ts # configureStore + middleware concat
|
|
15
|
+
│ ├── rootReducer.ts # combineReducers (slices + API reducers)
|
|
16
|
+
│ ├── reduxHooks.ts # typed useAppDispatch / useAppSelector
|
|
17
|
+
│ └── invalidateCacheMiddleware.ts # cross-API tag invalidation
|
|
18
|
+
└── app/
|
|
19
|
+
└── App.tsx # overlays _shared App with <Provider>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Auth: set-once at login
|
|
23
|
+
|
|
24
|
+
Tokens are set on the axios instance ONCE at login (rsense pattern, not per-request):
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { setAuthToken } from '@react-vault/core/http';
|
|
28
|
+
import axiosInstance from '@/axiosconfig/axiosInstance';
|
|
29
|
+
|
|
30
|
+
// inside loginApi's onQueryStarted or login slice:
|
|
31
|
+
setAuthToken(axiosInstance, response.token);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
On 401, the instance's `onUnauthorized` callback clears the token and redirects to `/login`.
|
|
35
|
+
|
|
36
|
+
## Feature pattern
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// src/features/Foo/api.ts
|
|
40
|
+
import { createApi } from '@reduxjs/toolkit/query/react';
|
|
41
|
+
import axiosBaseQuery from '@/axiosconfig/baseQuery';
|
|
42
|
+
import { fooResponseSchema } from './schema';
|
|
43
|
+
import type { FooResponse, FooQuery, FooBody } from './types';
|
|
44
|
+
|
|
45
|
+
const fooApi = createApi({
|
|
46
|
+
reducerPath: 'fooApi',
|
|
47
|
+
baseQuery: axiosBaseQuery(),
|
|
48
|
+
tagTypes: ['Foo'],
|
|
49
|
+
endpoints: (builder) => ({
|
|
50
|
+
getFoos: builder.query<FooResponse, FooQuery>({
|
|
51
|
+
query: (arg) => ({ url: '/foo', method: 'GET', data: arg }),
|
|
52
|
+
transformResponse: (raw: unknown) => fooResponseSchema.parse(raw),
|
|
53
|
+
providesTags: ['Foo'],
|
|
54
|
+
}),
|
|
55
|
+
createFoo: builder.mutation<FooResponse, FooBody>({
|
|
56
|
+
query: (body) => ({
|
|
57
|
+
url: '/foo',
|
|
58
|
+
method: 'POST',
|
|
59
|
+
data: body,
|
|
60
|
+
showSuccessNotification: true,
|
|
61
|
+
showFailureNotification: true,
|
|
62
|
+
}),
|
|
63
|
+
transformResponse: (raw: unknown) => fooResponseSchema.parse(raw),
|
|
64
|
+
invalidatesTags: ['Foo'],
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const { useGetFoosQuery, useCreateFooMutation } = fooApi;
|
|
70
|
+
export default fooApi;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Register the API in `src/redux/rootReducer.ts` (reducer) and `src/redux/store.ts` (middleware).
|
|
74
|
+
|
|
75
|
+
## Bundled skills (when this variant is picked)
|
|
76
|
+
|
|
77
|
+
The CLI copies a curated set of RTK-pattern skills into the scaffolded project's `.claude/skills/`:
|
|
78
|
+
|
|
79
|
+
- `axios-auth`
|
|
80
|
+
- `constants-organization`
|
|
81
|
+
- `redux-store-integration`
|
|
82
|
+
- `rtk-query-api`
|
|
83
|
+
|
|
84
|
+
Use `/bfsi-feature MyFeature` (provided by the inlined toolkit) — it'll generate RTK-style scaffolding aligned with these skills.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTK variant App.tsx — overlays the _shared App.tsx with Redux Provider.
|
|
3
|
+
*
|
|
4
|
+
* If you need to change the global layout / routes / error boundary, do it
|
|
5
|
+
* here. This file replaces _shared/src/app/App.tsx during scaffold.
|
|
6
|
+
*/
|
|
7
|
+
import { Provider } from 'react-redux';
|
|
8
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
9
|
+
import { ErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
10
|
+
import { AppRoutes } from '../routes/index.js';
|
|
11
|
+
import store from '../redux/store.js';
|
|
12
|
+
|
|
13
|
+
export function App(): JSX.Element {
|
|
14
|
+
return (
|
|
15
|
+
<ErrorBoundary>
|
|
16
|
+
<Provider store={store}>
|
|
17
|
+
<BrowserRouter>
|
|
18
|
+
<AppRoutes />
|
|
19
|
+
</BrowserRouter>
|
|
20
|
+
</Provider>
|
|
21
|
+
</ErrorBoundary>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single axios instance shared across the app. Auth token is set ONCE at
|
|
3
|
+
* login via setAuthToken() from @react-vault/core/http (rsense-style,
|
|
4
|
+
* set-at-login — not injected per-request).
|
|
5
|
+
*
|
|
6
|
+
* Side-effect import of `./interceptor` wires the response interceptor for
|
|
7
|
+
* notifications + 401 handling.
|
|
8
|
+
*/
|
|
9
|
+
import { createAxios } from '@react-vault/core/http';
|
|
10
|
+
import { env } from '../env.js';
|
|
11
|
+
import './interceptor.js';
|
|
12
|
+
|
|
13
|
+
const axiosInstance = createAxios({
|
|
14
|
+
baseURL: env.VITE_API_BASE_URL,
|
|
15
|
+
timeoutMs: env.VITE_API_TIMEOUT_MS,
|
|
16
|
+
authHeaderName: env.VITE_AUTH_HEADER_NAME,
|
|
17
|
+
snakeCaseBackend: false, // flip to true if backend uses snake_case
|
|
18
|
+
onUnauthorized: () => {
|
|
19
|
+
// Token already cleared by core; route to login here.
|
|
20
|
+
if (typeof window !== 'undefined') {
|
|
21
|
+
window.location.href = '/login';
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export default axiosInstance;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom RTK Query baseQuery built on the shared axios instance.
|
|
3
|
+
* Mirrors rsense-react-org's baseQuery: same shape, same notification flags.
|
|
4
|
+
*
|
|
5
|
+
* Each endpoint passes:
|
|
6
|
+
* { url, method, data, params, headers, showSuccessNotification, showFailureNotification }
|
|
7
|
+
*/
|
|
8
|
+
import type { AxiosRequestConfig, AxiosResponse, ResponseType } from 'axios';
|
|
9
|
+
import type { BaseQueryFn } from '@reduxjs/toolkit/query';
|
|
10
|
+
import axiosInstance from './axiosInstance.js';
|
|
11
|
+
import type { ApiErrorShape } from './interceptor.js';
|
|
12
|
+
|
|
13
|
+
const GET = 'GET' as const;
|
|
14
|
+
|
|
15
|
+
const axiosBaseQuery =
|
|
16
|
+
(): BaseQueryFn<
|
|
17
|
+
{
|
|
18
|
+
url: string;
|
|
19
|
+
method?: AxiosRequestConfig['method'];
|
|
20
|
+
data?: AxiosRequestConfig['data'];
|
|
21
|
+
params?: AxiosRequestConfig['params'];
|
|
22
|
+
headers?: AxiosRequestConfig['headers'];
|
|
23
|
+
showSuccessNotification?: boolean;
|
|
24
|
+
showFailureNotification?: boolean;
|
|
25
|
+
responseType?: ResponseType;
|
|
26
|
+
},
|
|
27
|
+
unknown,
|
|
28
|
+
unknown
|
|
29
|
+
> =>
|
|
30
|
+
async ({
|
|
31
|
+
url,
|
|
32
|
+
method,
|
|
33
|
+
data,
|
|
34
|
+
params,
|
|
35
|
+
headers,
|
|
36
|
+
showSuccessNotification: _showSuccess = false,
|
|
37
|
+
showFailureNotification: _showFailure = false,
|
|
38
|
+
responseType,
|
|
39
|
+
}) => {
|
|
40
|
+
try {
|
|
41
|
+
const requestConfig: AxiosRequestConfig =
|
|
42
|
+
method === GET
|
|
43
|
+
? { url, method, params: params ?? data, headers }
|
|
44
|
+
: { url, method, data, params, headers };
|
|
45
|
+
|
|
46
|
+
if (responseType) {
|
|
47
|
+
requestConfig.responseType = responseType;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result: AxiosResponse = await axiosInstance(requestConfig);
|
|
51
|
+
|
|
52
|
+
// Wire notification dispatch here once you have the slice:
|
|
53
|
+
// if (showSuccessNotification) {
|
|
54
|
+
// store.dispatch(setNotification({ type: 'success', message: result.data?.message }));
|
|
55
|
+
// }
|
|
56
|
+
|
|
57
|
+
return { data: result.data };
|
|
58
|
+
} catch (axiosError) {
|
|
59
|
+
const error = axiosError as ApiErrorShape;
|
|
60
|
+
// if (showFailureNotification) {
|
|
61
|
+
// store.dispatch(setNotification({ type: 'error', message: ... }));
|
|
62
|
+
// }
|
|
63
|
+
return {
|
|
64
|
+
error: {
|
|
65
|
+
status: error.response?.status,
|
|
66
|
+
data: error.response?.data?.errors,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default axiosBaseQuery;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response interceptor for the shared axios instance. Imported for its side
|
|
3
|
+
* effect from axiosInstance.ts. Pattern mirrors rsense-react-org.
|
|
4
|
+
*
|
|
5
|
+
* - 401: clear local auth state (handled by createAxios's onUnauthorized
|
|
6
|
+
* callback), dispatch error notification.
|
|
7
|
+
* - Other 4xx/5xx: surface the server message via the notification slice.
|
|
8
|
+
*/
|
|
9
|
+
import type { AxiosError, AxiosResponse } from 'axios';
|
|
10
|
+
import axiosInstance from './axiosInstance.js';
|
|
11
|
+
// Wire your notification slice + store here. Placeholders below.
|
|
12
|
+
// import store from '../redux/store.js';
|
|
13
|
+
// import { setNotification } from '../shared/Notification/slice.js';
|
|
14
|
+
|
|
15
|
+
export interface ApiErrorShape {
|
|
16
|
+
config?: { url?: string };
|
|
17
|
+
response?: {
|
|
18
|
+
status?: number;
|
|
19
|
+
data?: {
|
|
20
|
+
errors?: Array<{ detail?: string; details?: string }> | Record<string, string[]>;
|
|
21
|
+
message?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
axiosInstance.interceptors.response.use(
|
|
27
|
+
(response: AxiosResponse) => response,
|
|
28
|
+
(error: AxiosError) => {
|
|
29
|
+
const err = error as unknown as ApiErrorShape;
|
|
30
|
+
const status = err.response?.status;
|
|
31
|
+
|
|
32
|
+
// Hook this up to your notification slice once you wire it.
|
|
33
|
+
// store.dispatch(setNotification({ type: 'error', message: extractMessage(err) }));
|
|
34
|
+
|
|
35
|
+
if (status === 401) {
|
|
36
|
+
// 401 is also handled by createAxios's onUnauthorized callback (clears token).
|
|
37
|
+
// Add your own redirect / dispatch here.
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Promise.reject(err);
|
|
41
|
+
},
|
|
42
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-API cache invalidation middleware. Pattern from rsense-react-org:
|
|
3
|
+
* when one API's mutation fulfills, invalidate tags on related APIs.
|
|
4
|
+
*
|
|
5
|
+
* Add invalidation rules below. The default impl is a no-op; specialise as
|
|
6
|
+
* cross-API relationships emerge.
|
|
7
|
+
*/
|
|
8
|
+
import type { Middleware } from '@reduxjs/toolkit';
|
|
9
|
+
|
|
10
|
+
const invalidateCacheMiddleware: Middleware = () => (next) => (action) => {
|
|
11
|
+
// Example pattern (uncomment + adapt):
|
|
12
|
+
//
|
|
13
|
+
// if (kycApi.endpoints.submitKyc.matchFulfilled(action)) {
|
|
14
|
+
// storeApi.dispatch(userApi.util.invalidateTags(['User']));
|
|
15
|
+
// }
|
|
16
|
+
|
|
17
|
+
return next(action);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default invalidateCacheMiddleware;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed Redux hooks — use these instead of plain useDispatch / useSelector.
|
|
3
|
+
* Same pattern as rsense-react-org.
|
|
4
|
+
*/
|
|
5
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
6
|
+
import type { TypedUseSelectorHook } from 'react-redux';
|
|
7
|
+
import type { RootState, AppDispatch } from './store.js';
|
|
8
|
+
|
|
9
|
+
export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
10
|
+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root reducer. Compose every slice + every RTK Query API's reducer here.
|
|
3
|
+
*
|
|
4
|
+
* As you add features, import the slice/api and add to the object below.
|
|
5
|
+
*/
|
|
6
|
+
import { combineReducers } from '@reduxjs/toolkit';
|
|
7
|
+
|
|
8
|
+
const rootReducer = combineReducers({
|
|
9
|
+
// Slices:
|
|
10
|
+
// login: loginReducer,
|
|
11
|
+
// notification: notificationReducer,
|
|
12
|
+
//
|
|
13
|
+
// RTK Query API reducers (by their reducerPath):
|
|
14
|
+
// [loginApi.reducerPath]: loginApi.reducer,
|
|
15
|
+
// [kycApi.reducerPath]: kycApi.reducer,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export default rootReducer;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redux store. Mirrors rsense-react-org's pattern:
|
|
3
|
+
* - configureStore with rootReducer
|
|
4
|
+
* - middleware concat with each feature API's middleware (add as you create them)
|
|
5
|
+
* - setupListeners for RTK Query refetch-on-focus / reconnect
|
|
6
|
+
* - devTools enabled
|
|
7
|
+
*
|
|
8
|
+
* As you add features, import their api and:
|
|
9
|
+
* 1. Register the reducer in ./rootReducer.ts
|
|
10
|
+
* 2. Add the middleware below
|
|
11
|
+
*/
|
|
12
|
+
import { configureStore } from '@reduxjs/toolkit';
|
|
13
|
+
import { setupListeners } from '@reduxjs/toolkit/query';
|
|
14
|
+
import rootReducer from './rootReducer.js';
|
|
15
|
+
|
|
16
|
+
const store = configureStore({
|
|
17
|
+
reducer: rootReducer,
|
|
18
|
+
middleware: (getDefaultMiddleware) =>
|
|
19
|
+
getDefaultMiddleware({
|
|
20
|
+
serializableCheck: {
|
|
21
|
+
ignoredActions: [],
|
|
22
|
+
},
|
|
23
|
+
}).concat([
|
|
24
|
+
// Append feature API middleware here, e.g.:
|
|
25
|
+
// loginApi.middleware,
|
|
26
|
+
// kycApi.middleware,
|
|
27
|
+
]),
|
|
28
|
+
devTools: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
setupListeners(store.dispatch);
|
|
32
|
+
|
|
33
|
+
export type AppDispatch = typeof store.dispatch;
|
|
34
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
35
|
+
|
|
36
|
+
export default store;
|