@mind-fold/open-flow 0.1.17 → 0.2.2
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/dist/configurators/templates.d.ts.map +1 -1
- package/dist/configurators/templates.js +17 -6
- package/dist/configurators/templates.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +82 -6
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/templates/commands/break-loop.txt +107 -0
- package/dist/templates/commands/check-cross-layer.txt +153 -0
- package/dist/templates/commands/finish-work.txt +129 -0
- package/dist/templates/commands/index.d.ts +9 -5
- package/dist/templates/commands/index.d.ts.map +1 -1
- package/dist/templates/commands/index.js +16 -5
- package/dist/templates/commands/index.js.map +1 -1
- package/dist/templates/commands/init-agent.txt +100 -9
- package/dist/templates/commands/sync-from-runtime.txt +140 -0
- package/dist/templates/markdown/flow.md.txt +96 -84
- package/dist/templates/markdown/index.d.ts +21 -4
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +27 -4
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/structure/backend/database-guidelines.md.txt +247 -0
- package/dist/templates/markdown/structure/backend/directory-structure.md.txt +153 -0
- package/dist/templates/markdown/structure/backend/error-handling.md.txt +257 -0
- package/dist/templates/markdown/structure/backend/index.md.txt +88 -0
- package/dist/templates/markdown/structure/backend/logging-guidelines.md.txt +212 -0
- package/dist/templates/markdown/structure/backend/quality-guidelines.md.txt +219 -0
- package/dist/templates/markdown/structure/backend/type-safety.md.txt +192 -0
- package/dist/templates/markdown/structure/flows/code-reuse-thinking-guide.md.txt +343 -0
- package/dist/templates/markdown/structure/flows/cross-layer-thinking-guide.md.txt +283 -0
- package/dist/templates/markdown/structure/flows/index.md.txt +133 -0
- package/dist/templates/markdown/structure/flows/pre-implementation-checklist.md.txt +182 -0
- package/dist/templates/markdown/structure/flows/spec-flow-template.md.txt +145 -0
- package/dist/templates/markdown/structure/frontend/component-guidelines.md.txt +335 -0
- package/dist/templates/markdown/structure/frontend/directory-structure.md.txt +172 -0
- package/dist/templates/markdown/structure/frontend/hook-guidelines.md.txt +287 -0
- package/dist/templates/markdown/structure/frontend/index.md.txt +91 -0
- package/dist/templates/markdown/structure/frontend/quality-guidelines.md.txt +274 -0
- package/dist/templates/markdown/structure/frontend/state-management.md.txt +293 -0
- package/dist/templates/markdown/structure/frontend/type-safety.md.txt +275 -0
- package/package.json +2 -2
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Hook Guidelines
|
|
2
|
+
|
|
3
|
+
> Query hooks, mutation hooks, and custom hooks patterns
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Query Hooks
|
|
8
|
+
|
|
9
|
+
Use React Query for data fetching.
|
|
10
|
+
|
|
11
|
+
### Basic Query Hook
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { useQuery } from '@tanstack/react-query';
|
|
15
|
+
import { api } from '@/lib/api-client';
|
|
16
|
+
import type { User } from '@/types';
|
|
17
|
+
|
|
18
|
+
export function useUser(userId: string) {
|
|
19
|
+
return useQuery({
|
|
20
|
+
queryKey: ['user', userId],
|
|
21
|
+
queryFn: () => api.users.get(userId),
|
|
22
|
+
enabled: !!userId, // Don't fetch if no userId
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Usage
|
|
27
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
28
|
+
const { data: user, isLoading, error } = useUser(userId);
|
|
29
|
+
|
|
30
|
+
if (isLoading) return <Spinner />;
|
|
31
|
+
if (error) return <Error message={error.message} />;
|
|
32
|
+
if (!user) return <NotFound />;
|
|
33
|
+
|
|
34
|
+
return <div>{user.name}</div>;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### List Query with Pagination
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
export function useUsers(page: number, pageSize: number = 20) {
|
|
42
|
+
return useQuery({
|
|
43
|
+
queryKey: ['users', { page, pageSize }],
|
|
44
|
+
queryFn: () => api.users.list({ page, pageSize }),
|
|
45
|
+
placeholderData: keepPreviousData, // Keep old data while fetching new
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Query Key Rules
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// ✅ GOOD: Include all dependencies in query key
|
|
54
|
+
queryKey: ['user', userId]
|
|
55
|
+
queryKey: ['users', { page, pageSize, filter }]
|
|
56
|
+
queryKey: ['posts', userId, 'comments']
|
|
57
|
+
|
|
58
|
+
// ❌ BAD: Missing dependencies
|
|
59
|
+
queryKey: ['user'] // Same key for different users!
|
|
60
|
+
queryKey: ['users'] // Same key for different pages!
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Mutation Hooks
|
|
66
|
+
|
|
67
|
+
For create, update, delete operations.
|
|
68
|
+
|
|
69
|
+
### Basic Mutation
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
73
|
+
|
|
74
|
+
export function useUpdateUser() {
|
|
75
|
+
const queryClient = useQueryClient();
|
|
76
|
+
|
|
77
|
+
return useMutation({
|
|
78
|
+
mutationFn: (data: { id: string; name: string }) =>
|
|
79
|
+
api.users.update(data.id, { name: data.name }),
|
|
80
|
+
|
|
81
|
+
onSuccess: (updatedUser) => {
|
|
82
|
+
// Invalidate and refetch
|
|
83
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
84
|
+
queryClient.invalidateQueries({ queryKey: ['user', updatedUser.id] });
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Usage
|
|
90
|
+
function EditUserForm({ user }: { user: User }) {
|
|
91
|
+
const updateUser = useUpdateUser();
|
|
92
|
+
|
|
93
|
+
const handleSubmit = (data: FormData) => {
|
|
94
|
+
updateUser.mutate(
|
|
95
|
+
{ id: user.id, name: data.name },
|
|
96
|
+
{
|
|
97
|
+
onSuccess: () => toast.success('User updated'),
|
|
98
|
+
onError: (error) => toast.error(error.message),
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<form onSubmit={handleSubmit}>
|
|
105
|
+
{/* ... */}
|
|
106
|
+
<button disabled={updateUser.isPending}>
|
|
107
|
+
{updateUser.isPending ? 'Saving...' : 'Save'}
|
|
108
|
+
</button>
|
|
109
|
+
</form>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Optimistic Updates
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
export function useToggleFavorite() {
|
|
118
|
+
const queryClient = useQueryClient();
|
|
119
|
+
|
|
120
|
+
return useMutation({
|
|
121
|
+
mutationFn: (postId: string) => api.posts.toggleFavorite(postId),
|
|
122
|
+
|
|
123
|
+
// Optimistically update before server responds
|
|
124
|
+
onMutate: async (postId) => {
|
|
125
|
+
await queryClient.cancelQueries({ queryKey: ['posts'] });
|
|
126
|
+
|
|
127
|
+
const previousPosts = queryClient.getQueryData(['posts']);
|
|
128
|
+
|
|
129
|
+
queryClient.setQueryData(['posts'], (old: Post[]) =>
|
|
130
|
+
old.map(post =>
|
|
131
|
+
post.id === postId
|
|
132
|
+
? { ...post, isFavorite: !post.isFavorite }
|
|
133
|
+
: post
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return { previousPosts };
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// Rollback on error
|
|
141
|
+
onError: (err, postId, context) => {
|
|
142
|
+
queryClient.setQueryData(['posts'], context?.previousPosts);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// Always refetch after error or success
|
|
146
|
+
onSettled: () => {
|
|
147
|
+
queryClient.invalidateQueries({ queryKey: ['posts'] });
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Custom Hooks
|
|
156
|
+
|
|
157
|
+
### State + Logic Encapsulation
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
export function useToggle(initialValue = false) {
|
|
161
|
+
const [value, setValue] = useState(initialValue);
|
|
162
|
+
|
|
163
|
+
const toggle = useCallback(() => setValue(v => !v), []);
|
|
164
|
+
const setTrue = useCallback(() => setValue(true), []);
|
|
165
|
+
const setFalse = useCallback(() => setValue(false), []);
|
|
166
|
+
|
|
167
|
+
return { value, toggle, setTrue, setFalse };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Usage
|
|
171
|
+
function Dialog() {
|
|
172
|
+
const { value: isOpen, setTrue: open, setFalse: close } = useToggle();
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
<button onClick={open}>Open</button>
|
|
177
|
+
{isOpen && <Modal onClose={close} />}
|
|
178
|
+
</>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Debounced Value
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
187
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
191
|
+
return () => clearTimeout(timer);
|
|
192
|
+
}, [value, delay]);
|
|
193
|
+
|
|
194
|
+
return debouncedValue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Usage
|
|
198
|
+
function SearchInput() {
|
|
199
|
+
const [query, setQuery] = useState('');
|
|
200
|
+
const debouncedQuery = useDebounce(query, 300);
|
|
201
|
+
|
|
202
|
+
const { data } = useSearch(debouncedQuery);
|
|
203
|
+
|
|
204
|
+
return <input value={query} onChange={e => setQuery(e.target.value)} />;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Local Storage
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
212
|
+
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
213
|
+
if (typeof window === 'undefined') return initialValue;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const item = window.localStorage.getItem(key);
|
|
217
|
+
return item ? JSON.parse(item) : initialValue;
|
|
218
|
+
} catch {
|
|
219
|
+
return initialValue;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const setValue = useCallback((value: T | ((val: T) => T)) => {
|
|
224
|
+
setStoredValue(prev => {
|
|
225
|
+
const valueToStore = value instanceof Function ? value(prev) : value;
|
|
226
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
227
|
+
return valueToStore;
|
|
228
|
+
});
|
|
229
|
+
}, [key]);
|
|
230
|
+
|
|
231
|
+
return [storedValue, setValue] as const;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Best Practices
|
|
238
|
+
|
|
239
|
+
### DO
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// Include all dependencies in query keys
|
|
243
|
+
queryKey: ['user', userId, { includeDetails }]
|
|
244
|
+
|
|
245
|
+
// Handle loading and error states
|
|
246
|
+
if (isLoading) return <Spinner />;
|
|
247
|
+
if (error) return <ErrorDisplay error={error} />;
|
|
248
|
+
|
|
249
|
+
// Use enabled option for conditional fetching
|
|
250
|
+
enabled: !!userId && isAuthenticated
|
|
251
|
+
|
|
252
|
+
// Memoize callbacks in custom hooks
|
|
253
|
+
const toggle = useCallback(() => setValue(v => !v), []);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### DON'T
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// Don't fetch in useEffect
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
fetch('/api/user').then(...) // ❌ Use useQuery instead
|
|
262
|
+
}, []);
|
|
263
|
+
|
|
264
|
+
// Don't ignore error states
|
|
265
|
+
const { data } = useUser(id);
|
|
266
|
+
return <div>{data.name}</div>; // ❌ data might be undefined
|
|
267
|
+
|
|
268
|
+
// Don't mutate query data directly
|
|
269
|
+
data.name = 'New Name'; // ❌ Use mutation
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Hook Naming
|
|
275
|
+
|
|
276
|
+
| Pattern | Name | Example |
|
|
277
|
+
|---------|------|---------|
|
|
278
|
+
| Query single | `use{Entity}` | `useUser` |
|
|
279
|
+
| Query list | `use{Entity}s` | `useUsers` |
|
|
280
|
+
| Mutation create | `useCreate{Entity}` | `useCreateUser` |
|
|
281
|
+
| Mutation update | `useUpdate{Entity}` | `useUpdateUser` |
|
|
282
|
+
| Mutation delete | `useDelete{Entity}` | `useDeleteUser` |
|
|
283
|
+
| Custom utility | `use{Action}` | `useDebounce`, `useToggle` |
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
**Language**: All documentation must be written in **English**.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Frontend Development Guidelines
|
|
2
|
+
|
|
3
|
+
> Best practices for frontend development with React
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This collection of guidelines covers essential patterns and best practices for building robust frontend applications.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Guidelines Index
|
|
14
|
+
|
|
15
|
+
### Core Patterns
|
|
16
|
+
|
|
17
|
+
| Guide | Description |
|
|
18
|
+
|-------|-------------|
|
|
19
|
+
| [Directory Structure](./directory-structure.md) | Module organization and file layout |
|
|
20
|
+
| [Type Safety](./type-safety.md) | TypeScript patterns, type inference |
|
|
21
|
+
| [Quality Guidelines](./quality-guidelines.md) | Forbidden patterns, code standards |
|
|
22
|
+
|
|
23
|
+
### React Patterns
|
|
24
|
+
|
|
25
|
+
| Guide | Description |
|
|
26
|
+
|-------|-------------|
|
|
27
|
+
| [Hook Guidelines](./hook-guidelines.md) | Query hooks, mutation hooks, custom hooks |
|
|
28
|
+
| [Component Guidelines](./component-guidelines.md) | Component patterns, semantic HTML, accessibility |
|
|
29
|
+
| [State Management](./state-management.md) | Local state, global state, server state |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick Reference
|
|
34
|
+
|
|
35
|
+
### Directory Structure
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
src/
|
|
39
|
+
├── components/ # Shared components
|
|
40
|
+
│ ├── ui/ # Base UI components
|
|
41
|
+
│ └── common/ # Common composite components
|
|
42
|
+
│
|
|
43
|
+
├── modules/ # Feature modules
|
|
44
|
+
│ └── {feature}/
|
|
45
|
+
│ ├── components/ # Feature-specific components
|
|
46
|
+
│ ├── hooks/ # Feature-specific hooks
|
|
47
|
+
│ └── types.ts # Feature types
|
|
48
|
+
│
|
|
49
|
+
├── hooks/ # Shared hooks
|
|
50
|
+
├── lib/ # Utilities
|
|
51
|
+
└── types/ # Shared types
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Key Rules
|
|
55
|
+
|
|
56
|
+
| Rule | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| Import types from API | Don't redefine backend types |
|
|
59
|
+
| Use semantic HTML | `<button>` not `<div role="button">` |
|
|
60
|
+
| No `any` type | Use `unknown` + type guards |
|
|
61
|
+
| No `!` assertions | Use null checks |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Getting Started
|
|
66
|
+
|
|
67
|
+
1. **New to this codebase?** Start with [Directory Structure](./directory-structure.md)
|
|
68
|
+
2. **Writing hooks?** Read [Hook Guidelines](./hook-guidelines.md)
|
|
69
|
+
3. **Building components?** Read [Component Guidelines](./component-guidelines.md)
|
|
70
|
+
4. **Before commit?** Check [Quality Guidelines](./quality-guidelines.md)
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Common Tasks
|
|
75
|
+
|
|
76
|
+
### Create a New Feature Module
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
mkdir -p src/modules/{feature}/{components,hooks}
|
|
80
|
+
touch src/modules/{feature}/types.ts
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
See: [Directory Structure](./directory-structure.md)
|
|
84
|
+
|
|
85
|
+
### Add a Data Fetching Hook
|
|
86
|
+
|
|
87
|
+
See: [Hook Guidelines](./hook-guidelines.md) - Query Hooks section
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
**Language**: All documentation must be written in **English**.
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Quality Guidelines
|
|
2
|
+
|
|
3
|
+
> Code standards and forbidden patterns for frontend
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Before Every Commit
|
|
8
|
+
|
|
9
|
+
Run these checks before committing:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Must pass with 0 errors
|
|
13
|
+
pnpm lint
|
|
14
|
+
|
|
15
|
+
# Must pass with no type errors
|
|
16
|
+
pnpm type-check
|
|
17
|
+
|
|
18
|
+
# Run tests
|
|
19
|
+
pnpm test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Forbidden Patterns
|
|
25
|
+
|
|
26
|
+
### Any Type
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// ❌ FORBIDDEN
|
|
30
|
+
function process(data: any) { ... }
|
|
31
|
+
const result: any = await fetch(...);
|
|
32
|
+
|
|
33
|
+
// ✅ REQUIRED
|
|
34
|
+
function process(data: unknown) {
|
|
35
|
+
if (isValidData(data)) {
|
|
36
|
+
// Now data is typed
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Non-Null Assertions
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// ❌ FORBIDDEN
|
|
45
|
+
const name = user!.name;
|
|
46
|
+
const element = document.querySelector('.item')!;
|
|
47
|
+
|
|
48
|
+
// ✅ REQUIRED
|
|
49
|
+
if (!user) return null;
|
|
50
|
+
const name = user.name;
|
|
51
|
+
|
|
52
|
+
const element = document.querySelector('.item');
|
|
53
|
+
if (!element) return;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Type Assertions Without Validation
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// ❌ FORBIDDEN
|
|
60
|
+
const data = response as UserData;
|
|
61
|
+
const config = JSON.parse(str) as Config;
|
|
62
|
+
|
|
63
|
+
// ✅ REQUIRED
|
|
64
|
+
const data = userDataSchema.parse(response);
|
|
65
|
+
if (!isUserData(response)) throw new Error('Invalid data');
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Div Instead of Semantic HTML
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// ❌ FORBIDDEN
|
|
72
|
+
<div role="button" onClick={handleClick}>Click</div>
|
|
73
|
+
<div onClick={handleNavigate}>Go to page</div>
|
|
74
|
+
|
|
75
|
+
// ✅ REQUIRED
|
|
76
|
+
<button onClick={handleClick}>Click</button>
|
|
77
|
+
<Link href="/page">Go to page</Link>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Native img in Next.js
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// ❌ FORBIDDEN
|
|
84
|
+
<img src="/photo.jpg" alt="Photo" />
|
|
85
|
+
|
|
86
|
+
// ✅ REQUIRED
|
|
87
|
+
import Image from 'next/image';
|
|
88
|
+
<Image src="/photo.jpg" alt="Photo" width={200} height={200} />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### useEffect for Data Fetching
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// ❌ FORBIDDEN
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
fetch('/api/users')
|
|
97
|
+
.then(res => res.json())
|
|
98
|
+
.then(setUsers);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
// ✅ REQUIRED
|
|
102
|
+
const { data: users } = useQuery({
|
|
103
|
+
queryKey: ['users'],
|
|
104
|
+
queryFn: () => api.users.list(),
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Required Patterns
|
|
111
|
+
|
|
112
|
+
### Handle Loading States
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// ✅ REQUIRED: Handle all states
|
|
116
|
+
function UserList() {
|
|
117
|
+
const { data, isLoading, error } = useUsers();
|
|
118
|
+
|
|
119
|
+
if (isLoading) return <Skeleton />;
|
|
120
|
+
if (error) return <Error message={error.message} />;
|
|
121
|
+
if (!data?.length) return <Empty />;
|
|
122
|
+
|
|
123
|
+
return <List items={data} />;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Accessible Forms
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// ✅ REQUIRED: Labels for all inputs
|
|
131
|
+
<label htmlFor="email">Email</label>
|
|
132
|
+
<input id="email" type="email" aria-describedby="email-error" />
|
|
133
|
+
{error && <span id="email-error" role="alert">{error}</span>}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Error Boundaries
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// ✅ REQUIRED: Wrap feature modules
|
|
140
|
+
<ErrorBoundary fallback={<ErrorFallback />}>
|
|
141
|
+
<FeatureModule />
|
|
142
|
+
</ErrorBoundary>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Code Style
|
|
148
|
+
|
|
149
|
+
### Naming Conventions
|
|
150
|
+
|
|
151
|
+
| Type | Convention | Example |
|
|
152
|
+
|------|------------|---------|
|
|
153
|
+
| Components | PascalCase | `UserCard`, `SubmitButton` |
|
|
154
|
+
| Hooks | camelCase with `use` | `useUser`, `useToggle` |
|
|
155
|
+
| Files | kebab-case | `user-card.tsx`, `use-user.ts` |
|
|
156
|
+
| Types | PascalCase | `UserData`, `FormState` |
|
|
157
|
+
| Constants | SCREAMING_SNAKE | `MAX_FILE_SIZE` |
|
|
158
|
+
|
|
159
|
+
### Component File Structure
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// 1. Imports
|
|
163
|
+
import { useState } from 'react';
|
|
164
|
+
import { Button } from '@/components/ui';
|
|
165
|
+
|
|
166
|
+
// 2. Types
|
|
167
|
+
interface Props {
|
|
168
|
+
user: User;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 3. Component
|
|
172
|
+
export function UserCard({ user }: Props) {
|
|
173
|
+
// Hooks first
|
|
174
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
175
|
+
|
|
176
|
+
// Derived state
|
|
177
|
+
const displayName = user.nickname ?? user.name;
|
|
178
|
+
|
|
179
|
+
// Event handlers
|
|
180
|
+
const handleClick = () => setIsOpen(true);
|
|
181
|
+
|
|
182
|
+
// Render
|
|
183
|
+
return (
|
|
184
|
+
<article>
|
|
185
|
+
<h3>{displayName}</h3>
|
|
186
|
+
<Button onClick={handleClick}>View</Button>
|
|
187
|
+
</article>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Performance
|
|
195
|
+
|
|
196
|
+
### Memoization
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// ✅ Memoize expensive calculations
|
|
200
|
+
const sortedItems = useMemo(
|
|
201
|
+
() => items.sort((a, b) => a.name.localeCompare(b.name)),
|
|
202
|
+
[items]
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// ✅ Memoize callbacks passed to children
|
|
206
|
+
const handleClick = useCallback(() => {
|
|
207
|
+
doSomething(id);
|
|
208
|
+
}, [id]);
|
|
209
|
+
|
|
210
|
+
// ❌ Don't memoize everything - only when needed
|
|
211
|
+
const name = useMemo(() => user.name, [user.name]); // Unnecessary
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Avoid Unnecessary Rerenders
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// ❌ BAD: Creates new object every render
|
|
218
|
+
<Component style={{ color: 'red' }} />
|
|
219
|
+
|
|
220
|
+
// ✅ GOOD: Stable reference
|
|
221
|
+
const style = { color: 'red' }; // Outside component
|
|
222
|
+
<Component style={style} />
|
|
223
|
+
|
|
224
|
+
// ✅ GOOD: Or use className
|
|
225
|
+
<Component className="text-red" />
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Accessibility Checklist
|
|
231
|
+
|
|
232
|
+
- [ ] All images have alt text
|
|
233
|
+
- [ ] All inputs have labels
|
|
234
|
+
- [ ] Interactive elements are focusable
|
|
235
|
+
- [ ] Color is not the only indicator
|
|
236
|
+
- [ ] Proper heading hierarchy
|
|
237
|
+
- [ ] Keyboard navigation works
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Testing Requirements
|
|
242
|
+
|
|
243
|
+
### What to Test
|
|
244
|
+
|
|
245
|
+
- User interactions (click, type, submit)
|
|
246
|
+
- Error states
|
|
247
|
+
- Loading states
|
|
248
|
+
- Edge cases (empty data, long text)
|
|
249
|
+
|
|
250
|
+
### What NOT to Test
|
|
251
|
+
|
|
252
|
+
- Implementation details
|
|
253
|
+
- CSS styling
|
|
254
|
+
- Third-party library internals
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Checklist
|
|
259
|
+
|
|
260
|
+
Before submitting for review:
|
|
261
|
+
|
|
262
|
+
- [ ] No TypeScript errors (`pnpm type-check`)
|
|
263
|
+
- [ ] No lint errors (`pnpm lint`)
|
|
264
|
+
- [ ] No `any` types
|
|
265
|
+
- [ ] No `!` assertions
|
|
266
|
+
- [ ] Semantic HTML used
|
|
267
|
+
- [ ] Loading/error states handled
|
|
268
|
+
- [ ] Images use Next.js Image
|
|
269
|
+
- [ ] Tests pass
|
|
270
|
+
- [ ] Accessibility basics covered
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
**Language**: All documentation must be written in **English**.
|