@devmunna/agent-skillkit 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 +21 -0
- package/README.md +147 -0
- package/bin/ai-skills.js +5 -0
- package/dist/cli/commands/add.d.ts +2 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/add.js +66 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +33 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +145 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +55 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +49 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +2 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +22 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/prompts/agent-selector.d.ts +3 -0
- package/dist/cli/prompts/agent-selector.d.ts.map +1 -0
- package/dist/cli/prompts/agent-selector.js +23 -0
- package/dist/cli/prompts/agent-selector.js.map +1 -0
- package/dist/cli/prompts/stack-selector.d.ts +3 -0
- package/dist/cli/prompts/stack-selector.d.ts.map +1 -0
- package/dist/cli/prompts/stack-selector.js +60 -0
- package/dist/cli/prompts/stack-selector.js.map +1 -0
- package/dist/core/config-manager.d.ts +20 -0
- package/dist/core/config-manager.d.ts.map +1 -0
- package/dist/core/config-manager.js +107 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/detector.d.ts +3 -0
- package/dist/core/detector.d.ts.map +1 -0
- package/dist/core/detector.js +50 -0
- package/dist/core/detector.js.map +1 -0
- package/dist/core/doctor.d.ts +12 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +102 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/skill-registry.d.ts +11 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +174 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/skill-resolver.d.ts +3 -0
- package/dist/core/skill-resolver.d.ts.map +1 -0
- package/dist/core/skill-resolver.js +36 -0
- package/dist/core/skill-resolver.js.map +1 -0
- package/dist/core/validator.d.ts +13 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +99 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/generators/agent-installer.d.ts +5 -0
- package/dist/generators/agent-installer.d.ts.map +1 -0
- package/dist/generators/agent-installer.js +20 -0
- package/dist/generators/agent-installer.js.map +1 -0
- package/dist/generators/agents-md.d.ts +3 -0
- package/dist/generators/agents-md.d.ts.map +1 -0
- package/dist/generators/agents-md.js +70 -0
- package/dist/generators/agents-md.js.map +1 -0
- package/dist/generators/claude-md.d.ts +3 -0
- package/dist/generators/claude-md.d.ts.map +1 -0
- package/dist/generators/claude-md.js +47 -0
- package/dist/generators/claude-md.js.map +1 -0
- package/dist/generators/skill-generator.d.ts +5 -0
- package/dist/generators/skill-generator.d.ts.map +1 -0
- package/dist/generators/skill-generator.js +34 -0
- package/dist/generators/skill-generator.js.map +1 -0
- package/dist/generators/workflows.d.ts +3 -0
- package/dist/generators/workflows.d.ts.map +1 -0
- package/dist/generators/workflows.js +57 -0
- package/dist/generators/workflows.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/file-utils.d.ts +12 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +39 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +73 -0
- package/skills/clean-architecture/SKILL.md +324 -0
- package/skills/express-mvc-prisma/SKILL.md +168 -0
- package/skills/express-mvc-prisma/references/auth.md +190 -0
- package/skills/express-mvc-prisma/references/boilerplate.md +196 -0
- package/skills/express-mvc-prisma/references/error-handling.md +121 -0
- package/skills/express-mvc-prisma/references/module-scaffold.md +253 -0
- package/skills/express-mvc-prisma/references/prisma-setup.md +97 -0
- package/skills/express-mvc-prisma/references/response-helpers.md +157 -0
- package/skills/express-mvc-prisma/references/zod-validation.md +157 -0
- package/skills/fastify-rest/SKILL.md +287 -0
- package/skills/mongoose-odm/SKILL.md +281 -0
- package/skills/nextjs-fullstack/SKILL.md +328 -0
- package/skills/nextjs-fullstack/references/auth.md +270 -0
- package/skills/nextjs-fullstack/references/caching.md +157 -0
- package/skills/nextjs-fullstack/references/route-handlers.md +194 -0
- package/skills/nextjs-fullstack/references/server-actions.md +214 -0
- package/skills/nextjs-fullstack/references/server-components.md +190 -0
- package/skills/node-base/SKILL.md +139 -0
- package/skills/prisma-orm/SKILL.md +334 -0
- package/skills/react-feature-arch/SKILL.md +208 -0
- package/skills/react-feature-arch/references/api-layer.md +110 -0
- package/skills/react-feature-arch/references/components.md +192 -0
- package/skills/react-feature-arch/references/data-fetching.md +198 -0
- package/skills/react-feature-arch/references/forms.md +194 -0
- package/skills/react-feature-arch/references/routing.md +148 -0
- package/skills/react-feature-arch/references/state-management.md +107 -0
- package/skills/tailwind-css/SKILL.md +236 -0
- package/skills/tailwind-css/references/components.md +340 -0
- package/skills/tailwind-css/references/design-tokens.md +230 -0
- package/skills/tailwind-css/references/patterns.md +375 -0
- package/skills/tailwind-css/references/setup.md +165 -0
- package/skills/zod-validation/SKILL.md +267 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-feature-arch
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: >
|
|
5
|
+
Use this skill for any React project task: component design, hooks, state management, API integration, routing, forms, or folder structure. Triggers: "React component", "custom hook", "manage state", "fetch data", "React Router", "Zustand store", "TanStack Query", "form validation", "feature architecture", "React 19", "useActionState", "useOptimistic".
|
|
6
|
+
stack: [react, typescript, vite]
|
|
7
|
+
depends: []
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# React 19 — Production Feature Architecture
|
|
11
|
+
|
|
12
|
+
**Version target:** React 19 · React Router v7 · TanStack Query v5 · Zustand v5 · Vite 6 · TypeScript 5 · Zod v4
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Tech Stack
|
|
17
|
+
|
|
18
|
+
| Layer | Package |
|
|
19
|
+
|---|---|
|
|
20
|
+
| UI | react v19 + react-dom v19 |
|
|
21
|
+
| Routing | react-router v7 |
|
|
22
|
+
| Server state | @tanstack/react-query v5 |
|
|
23
|
+
| Global UI state | zustand v5 |
|
|
24
|
+
| Forms | react-hook-form v7 + @hookform/resolvers |
|
|
25
|
+
| Validation | zod v4 |
|
|
26
|
+
| HTTP | axios v1 |
|
|
27
|
+
| Build | vite v6 + @vitejs/plugin-react |
|
|
28
|
+
| Language | TypeScript 5 |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Feature Folder Structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
src/
|
|
36
|
+
├── app/
|
|
37
|
+
│ ├── App.tsx
|
|
38
|
+
│ ├── router.tsx # createBrowserRouter — all routes
|
|
39
|
+
│ └── providers.tsx # QueryClientProvider, Suspense, ErrorBoundary
|
|
40
|
+
├── features/ # One folder per domain
|
|
41
|
+
│ └── {feature}/
|
|
42
|
+
│ ├── {feature}.page.tsx # Route component — layout + composition only
|
|
43
|
+
│ ├── {feature}.api.ts # Axios API calls (all HTTP here)
|
|
44
|
+
│ ├── {feature}.hooks.ts # TanStack Query hooks (useQuery, useMutation)
|
|
45
|
+
│ ├── {feature}.schema.ts # Zod schemas + derived TypeScript types
|
|
46
|
+
│ ├── components/ # Feature-specific UI components
|
|
47
|
+
│ │ ├── {Feature}List.tsx
|
|
48
|
+
│ │ └── {Feature}Form.tsx
|
|
49
|
+
│ └── index.ts # Public barrel export
|
|
50
|
+
├── components/
|
|
51
|
+
│ ├── ui/ # Shared primitive components
|
|
52
|
+
│ └── layout/ # Layout + ProtectedRoute
|
|
53
|
+
├── lib/
|
|
54
|
+
│ ├── axios.ts # Axios instance + interceptors
|
|
55
|
+
│ └── query-client.ts # TanStack QueryClient singleton
|
|
56
|
+
├── store/ # Zustand stores (UI state only)
|
|
57
|
+
│ ├── auth.store.ts
|
|
58
|
+
│ └── ui.store.ts
|
|
59
|
+
├── types/ # Global TypeScript types
|
|
60
|
+
└── utils/ # Pure utility functions
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Rules:**
|
|
64
|
+
- Import from feature barrels (`@/features/users`), never deep into another feature's internals
|
|
65
|
+
- Page components own data-fetching via hooks; they compose child components and pass data down
|
|
66
|
+
- All HTTP calls live in `{feature}.api.ts` — never call axios directly in hooks or components
|
|
67
|
+
- Zod schemas in `{feature}.schema.ts` — TypeScript types derived with `z.infer<>`, shared by forms and API
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## React 19 — New Hooks
|
|
72
|
+
|
|
73
|
+
### useActionState — form actions with pending state
|
|
74
|
+
```tsx
|
|
75
|
+
import { useActionState } from 'react';
|
|
76
|
+
import { createUserSchema } from './users.schema';
|
|
77
|
+
import { usersApi } from './users.api';
|
|
78
|
+
|
|
79
|
+
async function submitAction(_prev: any, formData: FormData) {
|
|
80
|
+
const result = createUserSchema.safeParse({
|
|
81
|
+
name: formData.get('name'),
|
|
82
|
+
email: formData.get('email'),
|
|
83
|
+
});
|
|
84
|
+
if (!result.success) return { error: result.error.issues[0].message };
|
|
85
|
+
try {
|
|
86
|
+
await usersApi.create(result.data);
|
|
87
|
+
return { success: true };
|
|
88
|
+
} catch (err: any) {
|
|
89
|
+
return { error: err.message ?? 'Failed' };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function CreateUserForm() {
|
|
94
|
+
const [state, formAction, isPending] = useActionState(submitAction, null);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<form action={formAction}>
|
|
98
|
+
<input name="name" required />
|
|
99
|
+
<input name="email" type="email" required />
|
|
100
|
+
{state?.error && <p className="error">{state.error}</p>}
|
|
101
|
+
<button type="submit" disabled={isPending}>
|
|
102
|
+
{isPending ? 'Creating...' : 'Create'}
|
|
103
|
+
</button>
|
|
104
|
+
</form>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### useOptimistic — optimistic UI
|
|
110
|
+
```tsx
|
|
111
|
+
import { useOptimistic, useTransition } from 'react';
|
|
112
|
+
|
|
113
|
+
export function TodoList({ todos, onAdd }: { todos: Todo[]; onAdd: (t: Todo) => Promise<void> }) {
|
|
114
|
+
const [optimisticTodos, addOptimistic] = useOptimistic(
|
|
115
|
+
todos,
|
|
116
|
+
(state, newTodo: Todo) => [...state, { ...newTodo, pending: true }],
|
|
117
|
+
);
|
|
118
|
+
const [, startTransition] = useTransition();
|
|
119
|
+
|
|
120
|
+
const handleAdd = (title: string) => {
|
|
121
|
+
startTransition(async () => {
|
|
122
|
+
addOptimistic({ id: 'temp', title, done: false });
|
|
123
|
+
await onAdd({ id: 'temp', title, done: false });
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return <ul>{optimisticTodos.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### use() — read Context without useContext
|
|
132
|
+
```tsx
|
|
133
|
+
import { use } from 'react';
|
|
134
|
+
import { ThemeContext } from '@/app/providers';
|
|
135
|
+
|
|
136
|
+
export function ThemedCard({ children }: { children: React.ReactNode }) {
|
|
137
|
+
const theme = use(ThemeContext); // replaces useContext(ThemeContext)
|
|
138
|
+
return <div className={`card-${theme}`}>{children}</div>;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Component Rules
|
|
145
|
+
|
|
146
|
+
- Function components only — no class components
|
|
147
|
+
- Named exports preferred over default (better refactoring + tree-shaking)
|
|
148
|
+
- Single responsibility: one component = one visual concern
|
|
149
|
+
- Presentation components receive data via props — no direct API calls or store access
|
|
150
|
+
- If a component exceeds ~100 lines, extract sub-components
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
interface UserCardProps {
|
|
154
|
+
user: User;
|
|
155
|
+
onDelete: (id: string) => void;
|
|
156
|
+
isDeleting?: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function UserCard({ user, onDelete, isDeleting = false }: UserCardProps) {
|
|
160
|
+
return (
|
|
161
|
+
<div className="card">
|
|
162
|
+
<h3>{user.name}</h3>
|
|
163
|
+
<p>{user.email}</p>
|
|
164
|
+
<button onClick={() => onDelete(user.id)} disabled={isDeleting}>
|
|
165
|
+
{isDeleting ? 'Deleting...' : 'Delete'}
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## State Management Strategy
|
|
175
|
+
|
|
176
|
+
| State type | Tool |
|
|
177
|
+
|---|---|
|
|
178
|
+
| Server data (API responses) | TanStack Query v5 |
|
|
179
|
+
| Global UI (auth session, theme, sidebar) | Zustand v5 |
|
|
180
|
+
| Form state | React Hook Form v7 |
|
|
181
|
+
| URL state (filters, pagination, tabs) | URL search params |
|
|
182
|
+
| Component-local state | useState / useReducer |
|
|
183
|
+
|
|
184
|
+
**Rule:** Never put API response data in Zustand. Zustand is for UI-only global state. TanStack Query owns all server state including caching, invalidation, and background refetching.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Key Patterns
|
|
189
|
+
|
|
190
|
+
- `queryOptions()` helper for reusable, type-safe query definitions
|
|
191
|
+
- Mutations call `queryClient.invalidateQueries` on success to refresh stale data
|
|
192
|
+
- Axios interceptors set auth header from Zustand store + handle 401 globally
|
|
193
|
+
- `useShallow` from Zustand when selecting multiple fields to prevent extra re-renders
|
|
194
|
+
- Always `select` in `useQuery` to narrow returned shape when only part of data is needed
|
|
195
|
+
- Lazy-load route components: `const UsersPage = lazy(() => import('./users.page'))`
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Core References
|
|
200
|
+
|
|
201
|
+
| Topic | File |
|
|
202
|
+
|---|---|
|
|
203
|
+
| Axios instance + feature API files | `references/api-layer.md` |
|
|
204
|
+
| TanStack Query v5 — queryOptions, hooks, mutations | `references/data-fetching.md` |
|
|
205
|
+
| Zustand v5 — auth store, UI store | `references/state-management.md` |
|
|
206
|
+
| React Hook Form + Zod v4 forms | `references/forms.md` |
|
|
207
|
+
| React Router v7 — createBrowserRouter, ProtectedRoute | `references/routing.md` |
|
|
208
|
+
| Component patterns + React 19 hooks | `references/components.md` |
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# API Layer Reference
|
|
2
|
+
|
|
3
|
+
All HTTP calls live in `{feature}.api.ts`. Never import axios directly in hooks or components.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## src/lib/axios.ts — Axios Instance
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import { useAuthStore } from '@/store/auth.store';
|
|
12
|
+
|
|
13
|
+
export const apiClient = axios.create({
|
|
14
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
timeout: 10_000,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Attach Bearer token to every request
|
|
20
|
+
apiClient.interceptors.request.use((config) => {
|
|
21
|
+
const token = useAuthStore.getState().token;
|
|
22
|
+
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
23
|
+
return config;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Unwrap data.data + handle 401 globally
|
|
27
|
+
apiClient.interceptors.response.use(
|
|
28
|
+
(res) => res.data, // callers receive the payload directly (no res.data wrapper)
|
|
29
|
+
(err) => {
|
|
30
|
+
if (err.response?.status === 401) {
|
|
31
|
+
useAuthStore.getState().logout();
|
|
32
|
+
}
|
|
33
|
+
return Promise.reject(err.response?.data ?? err);
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## .env
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
VITE_API_URL=http://localhost:3000/api/v1
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Feature API File — {feature}.api.ts
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// features/users/users.api.ts
|
|
52
|
+
import { apiClient } from '@/lib/axios';
|
|
53
|
+
import type { CreateUserDto, UpdateUserDto, User } from './users.schema';
|
|
54
|
+
|
|
55
|
+
export interface PaginatedResponse<T> {
|
|
56
|
+
data: T[];
|
|
57
|
+
meta: {
|
|
58
|
+
total: number;
|
|
59
|
+
page: number;
|
|
60
|
+
limit: number;
|
|
61
|
+
totalPages: number;
|
|
62
|
+
hasNext: boolean;
|
|
63
|
+
hasPrev: boolean;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ListParams {
|
|
68
|
+
page?: number;
|
|
69
|
+
limit?: number;
|
|
70
|
+
search?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const usersApi = {
|
|
74
|
+
getAll: (params: ListParams) =>
|
|
75
|
+
apiClient.get<never, PaginatedResponse<User>>('/users', { params }),
|
|
76
|
+
|
|
77
|
+
getById: (id: string) =>
|
|
78
|
+
apiClient.get<never, User>(`/users/${id}`),
|
|
79
|
+
|
|
80
|
+
create: (data: CreateUserDto) =>
|
|
81
|
+
apiClient.post<never, User>('/users', data),
|
|
82
|
+
|
|
83
|
+
update: (id: string, data: UpdateUserDto) =>
|
|
84
|
+
apiClient.patch<never, User>(`/users/${id}`, data),
|
|
85
|
+
|
|
86
|
+
remove: (id: string) =>
|
|
87
|
+
apiClient.delete<never, void>(`/users/${id}`),
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Auth API
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// features/auth/auth.api.ts
|
|
97
|
+
import { apiClient } from '@/lib/axios';
|
|
98
|
+
import type { LoginDto, RegisterDto, User } from './auth.schema';
|
|
99
|
+
|
|
100
|
+
export const authApi = {
|
|
101
|
+
login: (data: LoginDto) =>
|
|
102
|
+
apiClient.post<never, { user: User; token: string }>('/auth/login', data),
|
|
103
|
+
|
|
104
|
+
register: (data: RegisterDto) =>
|
|
105
|
+
apiClient.post<never, { user: User; token: string }>('/auth/register', data),
|
|
106
|
+
|
|
107
|
+
me: () =>
|
|
108
|
+
apiClient.get<never, User>('/auth/me'),
|
|
109
|
+
};
|
|
110
|
+
```
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Component Patterns Reference — React 19
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Feature Component Structure
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// features/users/components/UserList.tsx
|
|
9
|
+
import type { User } from '../users.schema';
|
|
10
|
+
import { UserCard } from './UserCard';
|
|
11
|
+
|
|
12
|
+
interface UserListProps {
|
|
13
|
+
users: User[];
|
|
14
|
+
onDelete?: (id: string) => void;
|
|
15
|
+
isDeleting?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function UserList({ users, onDelete, isDeleting }: UserListProps) {
|
|
19
|
+
if (users.length === 0) return <p className="empty">No users found.</p>;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<ul className="user-list">
|
|
23
|
+
{users.map((user) => (
|
|
24
|
+
<UserCard
|
|
25
|
+
key={user.id}
|
|
26
|
+
user={user}
|
|
27
|
+
onDelete={onDelete}
|
|
28
|
+
isDeleting={isDeleting}
|
|
29
|
+
/>
|
|
30
|
+
))}
|
|
31
|
+
</ul>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Smart + Dumb Composition Pattern
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// SMART — page-level, owns data fetching
|
|
42
|
+
export function UsersPage() {
|
|
43
|
+
const [page, setPage] = useState(1);
|
|
44
|
+
const { data, isLoading } = useUsers({ page, limit: 10 });
|
|
45
|
+
const { mutate: deleteUser, isPending } = useDeleteUser();
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<UserList
|
|
49
|
+
users={data?.data ?? []}
|
|
50
|
+
isLoading={isLoading}
|
|
51
|
+
onDelete={deleteUser}
|
|
52
|
+
isDeleting={isPending}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// DUMB — purely presentational, receives all data via props
|
|
58
|
+
export function UserList({ users, isLoading, onDelete, isDeleting }: Props) {
|
|
59
|
+
if (isLoading) return <Skeleton count={5} />;
|
|
60
|
+
return users.map((u) => (
|
|
61
|
+
<UserCard key={u.id} user={u} onDelete={onDelete} isDeleting={isDeleting} />
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Performance Optimization
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { memo, useMemo, useCallback } from 'react';
|
|
72
|
+
|
|
73
|
+
// Memoize expensive computation
|
|
74
|
+
const sortedUsers = useMemo(
|
|
75
|
+
() => [...users].sort((a, b) => a.name.localeCompare(b.name)),
|
|
76
|
+
[users],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Stabilize callback passed to memoized children
|
|
80
|
+
const handleDelete = useCallback((id: string) => deleteUser(id), [deleteUser]);
|
|
81
|
+
|
|
82
|
+
// Memoize leaf component that gets stable props
|
|
83
|
+
export const UserCard = memo(function UserCard({ user, onDelete }: UserCardProps) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="card">
|
|
86
|
+
<h3>{user.name}</h3>
|
|
87
|
+
<button onClick={() => onDelete(user.id)}>Delete</button>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Custom Hooks
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// features/auth/auth.hooks.ts
|
|
99
|
+
import { useMutation } from '@tanstack/react-query';
|
|
100
|
+
import { useNavigate } from 'react-router';
|
|
101
|
+
import { useAuthStore } from '@/store/auth.store';
|
|
102
|
+
import { authApi } from './auth.api';
|
|
103
|
+
import { queryClient } from '@/lib/query-client';
|
|
104
|
+
|
|
105
|
+
export function useLogin() {
|
|
106
|
+
const login = useAuthStore((s) => s.login);
|
|
107
|
+
const navigate = useNavigate();
|
|
108
|
+
|
|
109
|
+
return useMutation({
|
|
110
|
+
mutationFn: authApi.login,
|
|
111
|
+
onSuccess: ({ user, token }) => {
|
|
112
|
+
login(user, token);
|
|
113
|
+
navigate('/dashboard');
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function useLogout() {
|
|
119
|
+
const logout = useAuthStore((s) => s.logout);
|
|
120
|
+
const navigate = useNavigate();
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
logout();
|
|
124
|
+
queryClient.clear(); // clear all cached server data on logout
|
|
125
|
+
navigate('/auth/login');
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Shared utility hook
|
|
130
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
131
|
+
const [debounced, setDebounced] = useState(value);
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const timer = setTimeout(() => setDebounced(value), delay);
|
|
135
|
+
return () => clearTimeout(timer);
|
|
136
|
+
}, [value, delay]);
|
|
137
|
+
|
|
138
|
+
return debounced;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Error Boundary
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
// components/ui/ErrorBoundary.tsx
|
|
148
|
+
import { Component, type ReactNode } from 'react';
|
|
149
|
+
|
|
150
|
+
interface State { hasError: boolean; message: string }
|
|
151
|
+
|
|
152
|
+
export class ErrorBoundary extends Component<
|
|
153
|
+
{ children: ReactNode; fallback?: ReactNode },
|
|
154
|
+
State
|
|
155
|
+
> {
|
|
156
|
+
state: State = { hasError: false, message: '' };
|
|
157
|
+
|
|
158
|
+
static getDerivedStateFromError(err: Error): State {
|
|
159
|
+
return { hasError: true, message: err.message };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
render() {
|
|
163
|
+
if (this.state.hasError) {
|
|
164
|
+
return this.props.fallback ?? (
|
|
165
|
+
<div className="error-boundary">
|
|
166
|
+
<h2>Something went wrong</h2>
|
|
167
|
+
<p>{this.state.message}</p>
|
|
168
|
+
<button onClick={() => this.setState({ hasError: false, message: '' })}>
|
|
169
|
+
Try again
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return this.props.children;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Feature Barrel Export (index.ts)
|
|
182
|
+
|
|
183
|
+
Export only the public surface. Never export internal helpers.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// features/users/index.ts
|
|
187
|
+
export { UsersPage } from './users.page';
|
|
188
|
+
export { UserList } from './components/UserList';
|
|
189
|
+
export { CreateUserForm } from './components/CreateUserForm';
|
|
190
|
+
export { useUsers, useUser, useCreateUser, useDeleteUser } from './users.hooks';
|
|
191
|
+
export type { User, CreateUserDto, UpdateUserDto } from './users.schema';
|
|
192
|
+
```
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# TanStack Query v5 Reference
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## src/lib/query-client.ts
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
9
|
+
|
|
10
|
+
export const queryClient = new QueryClient({
|
|
11
|
+
defaultOptions: {
|
|
12
|
+
queries: {
|
|
13
|
+
staleTime: 1000 * 60 * 5, // 5 min before background refetch
|
|
14
|
+
retry: 1,
|
|
15
|
+
refetchOnWindowFocus: false,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## src/app/providers.tsx
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
27
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
28
|
+
import { queryClient } from '@/lib/query-client';
|
|
29
|
+
|
|
30
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
31
|
+
return (
|
|
32
|
+
<QueryClientProvider client={queryClient}>
|
|
33
|
+
{children}
|
|
34
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
35
|
+
</QueryClientProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## queryOptions() — Reusable Query Definitions (v5)
|
|
43
|
+
|
|
44
|
+
Define query options once, reuse in `useQuery`, `useSuspenseQuery`, and `queryClient.prefetchQuery`.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// features/users/users.hooks.ts
|
|
48
|
+
import {
|
|
49
|
+
queryOptions,
|
|
50
|
+
useQuery,
|
|
51
|
+
useSuspenseQuery,
|
|
52
|
+
useMutation,
|
|
53
|
+
useQueryClient,
|
|
54
|
+
} from '@tanstack/react-query';
|
|
55
|
+
import { usersApi, type ListParams } from './users.api';
|
|
56
|
+
import type { CreateUserDto, UpdateUserDto } from './users.schema';
|
|
57
|
+
|
|
58
|
+
export const userQueries = {
|
|
59
|
+
list: (params: ListParams) =>
|
|
60
|
+
queryOptions({
|
|
61
|
+
queryKey: ['users', 'list', params],
|
|
62
|
+
queryFn: () => usersApi.getAll(params),
|
|
63
|
+
}),
|
|
64
|
+
|
|
65
|
+
detail: (id: string) =>
|
|
66
|
+
queryOptions({
|
|
67
|
+
queryKey: ['users', id],
|
|
68
|
+
queryFn: () => usersApi.getById(id),
|
|
69
|
+
enabled: !!id,
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Hooks — List, Detail, Mutations
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// List with pagination
|
|
80
|
+
export function useUsers(params: ListParams) {
|
|
81
|
+
return useQuery(userQueries.list(params));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Detail
|
|
85
|
+
export function useUser(id: string) {
|
|
86
|
+
return useQuery(userQueries.detail(id));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Suspense variant — throws while loading, use inside <Suspense>
|
|
90
|
+
export function useUserSuspense(id: string) {
|
|
91
|
+
return useSuspenseQuery(userQueries.detail(id));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create
|
|
95
|
+
export function useCreateUser() {
|
|
96
|
+
const queryClient = useQueryClient();
|
|
97
|
+
|
|
98
|
+
return useMutation({
|
|
99
|
+
mutationFn: (data: CreateUserDto) => usersApi.create(data),
|
|
100
|
+
onSuccess: () => {
|
|
101
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Update
|
|
107
|
+
export function useUpdateUser() {
|
|
108
|
+
const queryClient = useQueryClient();
|
|
109
|
+
|
|
110
|
+
return useMutation({
|
|
111
|
+
mutationFn: ({ id, data }: { id: string; data: UpdateUserDto }) =>
|
|
112
|
+
usersApi.update(id, data),
|
|
113
|
+
onSuccess: (_res, { id }) => {
|
|
114
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
115
|
+
queryClient.invalidateQueries({ queryKey: ['users', id] });
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Delete with optimistic update
|
|
121
|
+
export function useDeleteUser() {
|
|
122
|
+
const queryClient = useQueryClient();
|
|
123
|
+
|
|
124
|
+
return useMutation({
|
|
125
|
+
mutationFn: (id: string) => usersApi.remove(id),
|
|
126
|
+
onMutate: async (id) => {
|
|
127
|
+
await queryClient.cancelQueries({ queryKey: ['users'] });
|
|
128
|
+
const snapshot = queryClient.getQueryData(userQueries.list({ page: 1, limit: 10 }).queryKey);
|
|
129
|
+
// Optimistically remove from list
|
|
130
|
+
queryClient.setQueriesData({ queryKey: ['users', 'list'] }, (old: any) =>
|
|
131
|
+
old ? { ...old, data: old.data.filter((u: any) => u.id !== id) } : old,
|
|
132
|
+
);
|
|
133
|
+
return { snapshot };
|
|
134
|
+
},
|
|
135
|
+
onError: (_err, _id, context) => {
|
|
136
|
+
if (context?.snapshot) {
|
|
137
|
+
queryClient.setQueryData(
|
|
138
|
+
userQueries.list({ page: 1, limit: 10 }).queryKey,
|
|
139
|
+
context.snapshot,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
onSettled: () => {
|
|
144
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Usage in Page Component
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
// features/users/users.page.tsx
|
|
156
|
+
import { useState } from 'react';
|
|
157
|
+
import { useUsers, useDeleteUser } from './users.hooks';
|
|
158
|
+
import { UserList } from './components/UserList';
|
|
159
|
+
import { Pagination } from '@/components/ui/Pagination';
|
|
160
|
+
|
|
161
|
+
export function UsersPage() {
|
|
162
|
+
const [page, setPage] = useState(1);
|
|
163
|
+
const { data, isLoading, error } = useUsers({ page, limit: 10 });
|
|
164
|
+
const { mutate: deleteUser, isPending: isDeleting } = useDeleteUser();
|
|
165
|
+
|
|
166
|
+
if (isLoading) return <div>Loading...</div>;
|
|
167
|
+
if (error) return <div>Error: {(error as any).message}</div>;
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<>
|
|
171
|
+
<UserList
|
|
172
|
+
users={data!.data}
|
|
173
|
+
onDelete={deleteUser}
|
|
174
|
+
isDeleting={isDeleting}
|
|
175
|
+
/>
|
|
176
|
+
<Pagination meta={data!.meta} onPageChange={setPage} />
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Prefetching (router loaders)
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
// Prefetch in router loader for instant navigation
|
|
188
|
+
import { queryClient } from '@/lib/query-client';
|
|
189
|
+
|
|
190
|
+
const router = createBrowserRouter([
|
|
191
|
+
{
|
|
192
|
+
path: '/users/:id',
|
|
193
|
+
loader: ({ params }) =>
|
|
194
|
+
queryClient.ensureQueryData(userQueries.detail(params.id!)),
|
|
195
|
+
element: <UserDetailPage />,
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
```
|