@yuaone/core 0.3.3 → 0.4.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/dist/agent-loop.d.ts +62 -0
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +705 -18
- package/dist/agent-loop.js.map +1 -1
- package/dist/background-agent.d.ts +110 -0
- package/dist/background-agent.d.ts.map +1 -0
- package/dist/background-agent.js +255 -0
- package/dist/background-agent.js.map +1 -0
- package/dist/coding-standards.d.ts +45 -0
- package/dist/coding-standards.d.ts.map +1 -0
- package/dist/coding-standards.js +1152 -0
- package/dist/coding-standards.js.map +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -6
- package/dist/constants.js.map +1 -1
- package/dist/context-manager.d.ts +6 -0
- package/dist/context-manager.d.ts.map +1 -1
- package/dist/context-manager.js +23 -4
- package/dist/context-manager.js.map +1 -1
- package/dist/index.d.ts +28 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/llm-client.d.ts +8 -3
- package/dist/llm-client.d.ts.map +1 -1
- package/dist/llm-client.js +64 -13
- package/dist/llm-client.js.map +1 -1
- package/dist/plugin-auto-loader.d.ts +108 -0
- package/dist/plugin-auto-loader.d.ts.map +1 -0
- package/dist/plugin-auto-loader.js +743 -0
- package/dist/plugin-auto-loader.js.map +1 -0
- package/dist/plugin-registry.d.ts +112 -0
- package/dist/plugin-registry.d.ts.map +1 -0
- package/dist/plugin-registry.js +319 -0
- package/dist/plugin-registry.js.map +1 -0
- package/dist/plugin-types.d.ts +388 -0
- package/dist/plugin-types.d.ts.map +1 -0
- package/dist/plugin-types.js +8 -0
- package/dist/plugin-types.js.map +1 -0
- package/dist/plugin-validator.d.ts +54 -0
- package/dist/plugin-validator.d.ts.map +1 -0
- package/dist/plugin-validator.js +129 -0
- package/dist/plugin-validator.js.map +1 -0
- package/dist/repo-knowledge-graph.d.ts +112 -0
- package/dist/repo-knowledge-graph.d.ts.map +1 -0
- package/dist/repo-knowledge-graph.js +561 -0
- package/dist/repo-knowledge-graph.js.map +1 -0
- package/dist/role-registry.js +1 -1
- package/dist/role-registry.js.map +1 -1
- package/dist/self-debug-loop.d.ts +257 -0
- package/dist/self-debug-loop.d.ts.map +1 -0
- package/dist/self-debug-loop.js +870 -0
- package/dist/self-debug-loop.js.map +1 -0
- package/dist/skill-learner.d.ts +136 -0
- package/dist/skill-learner.d.ts.map +1 -0
- package/dist/skill-learner.js +382 -0
- package/dist/skill-learner.js.map +1 -0
- package/dist/skill-loader.d.ts +90 -0
- package/dist/skill-loader.d.ts.map +1 -0
- package/dist/skill-loader.js +309 -0
- package/dist/skill-loader.js.map +1 -0
- package/dist/specialist-registry.d.ts +132 -0
- package/dist/specialist-registry.d.ts.map +1 -0
- package/dist/specialist-registry.js +413 -0
- package/dist/specialist-registry.js.map +1 -0
- package/dist/sub-agent-prompts.d.ts +45 -0
- package/dist/sub-agent-prompts.d.ts.map +1 -0
- package/dist/sub-agent-prompts.js +177 -0
- package/dist/sub-agent-prompts.js.map +1 -0
- package/dist/sub-agent-router.d.ts +75 -0
- package/dist/sub-agent-router.d.ts.map +1 -0
- package/dist/sub-agent-router.js +174 -0
- package/dist/sub-agent-router.js.map +1 -0
- package/dist/sub-agent.d.ts +48 -0
- package/dist/sub-agent.d.ts.map +1 -1
- package/dist/sub-agent.js +108 -5
- package/dist/sub-agent.js.map +1 -1
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +177 -7
- package/dist/system-prompt.js.map +1 -1
- package/dist/task-classifier.d.ts +25 -1
- package/dist/task-classifier.d.ts.map +1 -1
- package/dist/task-classifier.js +171 -1
- package/dist/task-classifier.js.map +1 -1
- package/dist/tool-planner.d.ts +160 -0
- package/dist/tool-planner.d.ts.map +1 -0
- package/dist/tool-planner.js +501 -0
- package/dist/tool-planner.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/plugins/git/patterns/branch-patterns.json +101 -0
- package/plugins/git/patterns/commit-patterns.json +186 -0
- package/plugins/git/plugin.yaml +128 -0
- package/plugins/git/skills/branch-strategy.md +172 -0
- package/plugins/git/skills/commit-conv.md +178 -0
- package/plugins/git/skills/conflict-resolve.md +159 -0
- package/plugins/git/skills/history-clean.md +199 -0
- package/plugins/git/skills/pr-review.md +196 -0
- package/plugins/git/strategies/conflict-resolve.json +244 -0
- package/plugins/git/strategies/release-flow.json +292 -0
- package/plugins/git/validators/rules.json +348 -0
- package/plugins/react/patterns/anti-patterns.json +88 -0
- package/plugins/react/patterns/components.json +80 -0
- package/plugins/react/patterns/hooks.json +72 -0
- package/plugins/react/plugin.yaml +229 -0
- package/plugins/react/skills/bugfix.md +208 -0
- package/plugins/react/skills/component-gen.md +206 -0
- package/plugins/react/skills/hook-extract.md +208 -0
- package/plugins/react/skills/ssr.md +256 -0
- package/plugins/react/skills/test.md +273 -0
- package/plugins/react/strategies/build-fix.json +43 -0
- package/plugins/react/strategies/hook-loop-fix.json +36 -0
- package/plugins/react/strategies/hydration-fix.json +42 -0
- package/plugins/react/validators/rules.json +92 -0
- package/plugins/typescript/patterns/best-practices.json +25 -0
- package/plugins/typescript/patterns/common-errors.json +32 -0
- package/plugins/typescript/plugin.yaml +74 -0
- package/plugins/typescript/skills/debug.md +23 -0
- package/plugins/typescript/skills/migration.md +24 -0
- package/plugins/typescript/skills/refactor.md +22 -0
- package/plugins/typescript/skills/strict-mode.md +23 -0
- package/plugins/typescript/strategies/strict-migration.json +37 -0
- package/plugins/typescript/strategies/type-error-fix.json +37 -0
- package/plugins/typescript/validators/rules.json +28 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# React Bugfix Skill
|
|
2
|
+
|
|
3
|
+
## Identity
|
|
4
|
+
- domain: react
|
|
5
|
+
- type: bugfix
|
|
6
|
+
- confidence: 0.85
|
|
7
|
+
- persona: Senior React engineer with 8+ years of production debugging experience. Expert in React internals, reconciliation algorithm, fiber architecture, and common failure modes across CSR/SSR environments.
|
|
8
|
+
|
|
9
|
+
## Known Error Patterns
|
|
10
|
+
|
|
11
|
+
### 1. Hydration Mismatch
|
|
12
|
+
- **symptoms**:
|
|
13
|
+
- "Text content does not match server-rendered HTML"
|
|
14
|
+
- "Hydration failed because the initial UI does not match what was rendered on the server"
|
|
15
|
+
- "There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering"
|
|
16
|
+
- Visual flicker on page load
|
|
17
|
+
- Content changes immediately after hydration
|
|
18
|
+
- **causes**:
|
|
19
|
+
- Direct access to `window`, `document`, `localStorage` during render
|
|
20
|
+
- `Date.now()`, `Math.random()`, or locale-dependent formatting in render path
|
|
21
|
+
- Conditional rendering based on `typeof window !== 'undefined'`
|
|
22
|
+
- Browser extensions injecting DOM nodes
|
|
23
|
+
- Different data between server and client (stale cache, time zones)
|
|
24
|
+
- **strategy**:
|
|
25
|
+
1. Move client-only code into `useEffect` so it runs only after hydration
|
|
26
|
+
2. Use `next/dynamic` with `{ ssr: false }` for client-only components
|
|
27
|
+
3. Use `suppressHydrationWarning` as a last resort (only for intentional mismatches like timestamps)
|
|
28
|
+
4. Create a `useIsClient()` hook: `const [isClient, setIsClient] = useState(false); useEffect(() => setIsClient(true), []); return isClient;`
|
|
29
|
+
5. For third-party components that access `window`, wrap in dynamic import
|
|
30
|
+
- **tools**: grep, file_read, file_edit, shell_exec
|
|
31
|
+
- **pitfalls**:
|
|
32
|
+
- Do NOT wrap everything in `useEffect` blindly -- this defeats SSR benefits
|
|
33
|
+
- `suppressHydrationWarning` only suppresses warnings, does not fix the actual mismatch
|
|
34
|
+
- Browser extensions (e.g., Grammarly, translation plugins) can cause hydration errors that are not your fault -- check if the issue reproduces in incognito mode
|
|
35
|
+
|
|
36
|
+
### 2. useEffect Infinite Loop
|
|
37
|
+
- **symptoms**:
|
|
38
|
+
- "Maximum update depth exceeded"
|
|
39
|
+
- "Too many re-renders. React limits the number of renders to prevent an infinite loop"
|
|
40
|
+
- Browser tab freezing or crashing
|
|
41
|
+
- CPU spike in devtools
|
|
42
|
+
- Rapid state updates visible in React DevTools profiler
|
|
43
|
+
- **causes**:
|
|
44
|
+
- Missing dependency array: `useEffect(() => { setState(val) })` (runs every render)
|
|
45
|
+
- Object/array in dependency array: `useEffect(() => {}, [{ a: 1 }])` (new reference every render)
|
|
46
|
+
- Setting state that is also a dependency: `useEffect(() => { setCount(count + 1) }, [count])`
|
|
47
|
+
- Stale closure capturing outdated values, causing repeated updates
|
|
48
|
+
- Calling a function that creates a new reference on every render as a dependency
|
|
49
|
+
- **strategy**:
|
|
50
|
+
1. Check if dependency array exists -- add `[]` for mount-only effects
|
|
51
|
+
2. Identify object/array deps -- stabilize with `useMemo` or `useCallback`
|
|
52
|
+
3. Use functional updater: `setCount(prev => prev + 1)` instead of `setCount(count + 1)`
|
|
53
|
+
4. Extract primitive values: `const { id } = obj; useEffect(() => {}, [id])` instead of `[obj]`
|
|
54
|
+
5. Use `useRef` for values that should not trigger re-renders
|
|
55
|
+
6. Consider `useReducer` for complex state logic to avoid multiple interdependent effects
|
|
56
|
+
- **tools**: file_read, grep, file_edit
|
|
57
|
+
- **pitfalls**:
|
|
58
|
+
- Do NOT disable `eslint-plugin-react-hooks` exhaustive-deps rule
|
|
59
|
+
- Do NOT use `// eslint-disable-next-line react-hooks/exhaustive-deps` without understanding why
|
|
60
|
+
- An empty `[]` dependency array is only correct for true mount-only effects
|
|
61
|
+
|
|
62
|
+
### 3. State Not Updating
|
|
63
|
+
- **symptoms**:
|
|
64
|
+
- UI does not reflect state change after `setState`
|
|
65
|
+
- `console.log` after `setState` shows old value
|
|
66
|
+
- State appears to "reset" or "revert"
|
|
67
|
+
- Conditional logic based on state executes wrong branch
|
|
68
|
+
- Multiple rapid updates only reflect the last one
|
|
69
|
+
- **causes**:
|
|
70
|
+
- Direct mutation: `state.items.push(newItem); setState(state)` (same reference, no re-render)
|
|
71
|
+
- Reading state immediately after `setState` (batched, asynchronous)
|
|
72
|
+
- Derived state anti-pattern: duplicating prop into state without sync
|
|
73
|
+
- Stale closure in event handler or timer callback
|
|
74
|
+
- React 18 automatic batching grouping updates unexpectedly
|
|
75
|
+
- **strategy**:
|
|
76
|
+
1. Always create new references: `setState(prev => [...prev, newItem])`
|
|
77
|
+
2. For objects: `setState(prev => ({ ...prev, [key]: value }))`
|
|
78
|
+
3. Use functional updater for sequential updates: `setCount(prev => prev + 1)`
|
|
79
|
+
4. For derived state, compute during render instead of syncing with `useEffect`
|
|
80
|
+
5. Use `flushSync` from `react-dom` only if you absolutely need synchronous updates (rare)
|
|
81
|
+
6. Consider Immer (`useImmer`) for deeply nested state mutations
|
|
82
|
+
- **tools**: file_read, file_edit
|
|
83
|
+
- **pitfalls**:
|
|
84
|
+
- Do NOT use `JSON.parse(JSON.stringify(state))` for deep cloning -- use structured clone or Immer
|
|
85
|
+
- Do NOT read state right after setting it and expect the new value
|
|
86
|
+
- Avoid `useEffect` to "sync" props to state -- this is almost always wrong
|
|
87
|
+
|
|
88
|
+
### 4. Key Prop Issues
|
|
89
|
+
- **symptoms**:
|
|
90
|
+
- "Each child in a list should have a unique 'key' prop"
|
|
91
|
+
- List items losing input focus on re-render
|
|
92
|
+
- Animations not working correctly in lists
|
|
93
|
+
- Wrong items being updated or deleted in lists
|
|
94
|
+
- Component state being shared between different list items
|
|
95
|
+
- **causes**:
|
|
96
|
+
- Using array index as key: `items.map((item, i) => <Item key={i} />)`
|
|
97
|
+
- Missing key prop entirely
|
|
98
|
+
- Non-unique keys (duplicate IDs in data)
|
|
99
|
+
- Key changing on every render (e.g., `key={Math.random()}`)
|
|
100
|
+
- Key not reflecting item identity (using wrong field)
|
|
101
|
+
- **strategy**:
|
|
102
|
+
1. Use a stable, unique identifier from data: `key={item.id}`
|
|
103
|
+
2. If no ID exists, generate one when creating the item (not during render)
|
|
104
|
+
3. For static lists that never reorder, index is acceptable
|
|
105
|
+
4. Use `crypto.randomUUID()` at item creation time, not render time
|
|
106
|
+
5. For compound keys: `key={\`${item.type}-${item.id}\`}`
|
|
107
|
+
- **tools**: grep, file_read, file_edit
|
|
108
|
+
- **pitfalls**:
|
|
109
|
+
- Index keys cause bugs when list is sorted, filtered, or items are inserted/removed
|
|
110
|
+
- `key={Math.random()}` forces remount on every render -- massive performance issue
|
|
111
|
+
- Keys must be stable across re-renders -- do NOT generate IDs in the render function
|
|
112
|
+
|
|
113
|
+
### 5. Memory Leaks
|
|
114
|
+
- **symptoms**:
|
|
115
|
+
- "Can't perform a React state update on an unmounted component"
|
|
116
|
+
- Increasing memory usage over time (check Performance tab)
|
|
117
|
+
- Stale data appearing after navigation
|
|
118
|
+
- Event listeners firing for unmounted components
|
|
119
|
+
- WebSocket connections staying open after unmount
|
|
120
|
+
- **causes**:
|
|
121
|
+
- Missing cleanup function in `useEffect`
|
|
122
|
+
- Not aborting fetch requests on unmount
|
|
123
|
+
- Not removing event listeners (`window.addEventListener` without cleanup)
|
|
124
|
+
- Not clearing timers (`setInterval`, `setTimeout`)
|
|
125
|
+
- Subscriptions (WebSocket, EventSource) not closed on unmount
|
|
126
|
+
- **strategy**:
|
|
127
|
+
1. Always return cleanup from `useEffect`:
|
|
128
|
+
```
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
fetch(url, { signal: controller.signal });
|
|
132
|
+
return () => controller.abort();
|
|
133
|
+
}, [url]);
|
|
134
|
+
```
|
|
135
|
+
2. Use `AbortController` for all fetch calls
|
|
136
|
+
3. Clear all timers: `return () => clearInterval(id);`
|
|
137
|
+
4. Remove event listeners: `return () => window.removeEventListener('resize', handler);`
|
|
138
|
+
5. Close WebSocket/EventSource connections in cleanup
|
|
139
|
+
6. For React 18 Strict Mode double-mount, ensure cleanup is idempotent
|
|
140
|
+
- **tools**: grep, file_read, file_edit
|
|
141
|
+
- **pitfalls**:
|
|
142
|
+
- React 18 Strict Mode in dev intentionally double-mounts -- this exposes leaks, do not suppress it
|
|
143
|
+
- The "unmounted component" warning was removed in React 18 but the leak still exists
|
|
144
|
+
- `AbortError` from aborted fetch is expected -- catch and ignore it
|
|
145
|
+
|
|
146
|
+
### 6. Conditional Hook Call
|
|
147
|
+
- **symptoms**:
|
|
148
|
+
- "React Hook is called conditionally. React Hooks must be called in the exact same order"
|
|
149
|
+
- "Rendered more hooks than during the previous render"
|
|
150
|
+
- "Rendered fewer hooks than expected"
|
|
151
|
+
- Cryptic errors after adding/removing hooks in conditionals
|
|
152
|
+
- **causes**:
|
|
153
|
+
- Hook inside `if` statement or ternary
|
|
154
|
+
- Hook after early return
|
|
155
|
+
- Hook inside loop or nested function
|
|
156
|
+
- Dynamic hook calls based on props
|
|
157
|
+
- **strategy**:
|
|
158
|
+
1. Move all hooks to the top level of the component, before any conditionals
|
|
159
|
+
2. Use the hook unconditionally, then conditionally use its result
|
|
160
|
+
3. For conditional effects: `useEffect(() => { if (condition) { /* ... */ } }, [condition])`
|
|
161
|
+
4. Split into separate components if hook logic is fundamentally conditional
|
|
162
|
+
5. For dynamic lists of hooks, restructure to use a single hook with array state
|
|
163
|
+
- **tools**: file_read, file_edit
|
|
164
|
+
- **pitfalls**:
|
|
165
|
+
- Hooks rely on call order -- React uses position to match hooks between renders
|
|
166
|
+
- Even if a condition is "always true", the linter cannot verify it -- restructure instead
|
|
167
|
+
- Custom hooks are still hooks -- they follow the same rules
|
|
168
|
+
|
|
169
|
+
### 7. Stale Closure
|
|
170
|
+
- **symptoms**:
|
|
171
|
+
- Event handler uses outdated state value
|
|
172
|
+
- `setTimeout`/`setInterval` callback sees initial state
|
|
173
|
+
- Click handler in a loop always captures last iteration value
|
|
174
|
+
- Async callback returns stale data after state changed
|
|
175
|
+
- **causes**:
|
|
176
|
+
- Closure captures the state value at the time of creation
|
|
177
|
+
- Event handler created in a previous render still references old state
|
|
178
|
+
- Timer callback captures stale variable
|
|
179
|
+
- **strategy**:
|
|
180
|
+
1. Use functional updater: `setState(prev => prev + 1)`
|
|
181
|
+
2. Use `useRef` to always have current value: `const countRef = useRef(count); countRef.current = count;`
|
|
182
|
+
3. Use `useCallback` with correct deps to recreate handler when deps change
|
|
183
|
+
4. For intervals, use a custom `useInterval` hook with ref-based callback
|
|
184
|
+
5. In event listeners, re-attach when deps change via useEffect cleanup
|
|
185
|
+
- **tools**: file_read, file_edit, grep
|
|
186
|
+
- **pitfalls**:
|
|
187
|
+
- Adding state to useEffect deps to fix stale closure can introduce infinite loops -- use refs for read-only access
|
|
188
|
+
- `useRef` does not trigger re-renders -- if you need both current value and re-render, combine ref + state
|
|
189
|
+
|
|
190
|
+
## Tool Sequence
|
|
191
|
+
1. **grep** -- Search for error pattern in source files (`**/*.tsx`, `**/*.jsx`)
|
|
192
|
+
2. **file_read** -- Read the identified file(s) to understand context
|
|
193
|
+
3. **grep** -- Search for related patterns (hook usage, state definitions, effect dependencies)
|
|
194
|
+
4. **file_read** -- Read related files (parent components, shared hooks, store)
|
|
195
|
+
5. **file_edit** -- Apply the fix with minimal changes
|
|
196
|
+
6. **shell_exec** -- Run `pnpm build` or `next build` to verify no build errors
|
|
197
|
+
7. **shell_exec** -- Run tests if available (`pnpm test` or `vitest run`)
|
|
198
|
+
8. **grep** -- Verify the error pattern no longer exists in build output
|
|
199
|
+
|
|
200
|
+
## Validation Checklist
|
|
201
|
+
- [ ] Error message no longer appears in console/build output
|
|
202
|
+
- [ ] `pnpm build` (or `next build`) passes without errors
|
|
203
|
+
- [ ] Existing tests pass (`pnpm test`)
|
|
204
|
+
- [ ] No new TypeScript errors (`tsc --noEmit`)
|
|
205
|
+
- [ ] No new ESLint warnings from `react-hooks/exhaustive-deps`
|
|
206
|
+
- [ ] Component renders correctly in both SSR and CSR
|
|
207
|
+
- [ ] No performance regression (no unnecessary re-renders introduced)
|
|
208
|
+
- [ ] Fix does not break other components that depend on the modified code
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# React Component Generator Skill
|
|
2
|
+
|
|
3
|
+
## Identity
|
|
4
|
+
- domain: react
|
|
5
|
+
- type: generator
|
|
6
|
+
- confidence: 0.90
|
|
7
|
+
- persona: Senior React architect specializing in component design patterns, atomic design methodology, and TypeScript-first component APIs. Expert in building reusable, accessible, and testable component libraries.
|
|
8
|
+
|
|
9
|
+
## Known Error Patterns
|
|
10
|
+
|
|
11
|
+
### 1. Missing TypeScript Props Interface
|
|
12
|
+
- **symptoms**:
|
|
13
|
+
- Props typed as `any` or missing entirely
|
|
14
|
+
- No autocomplete for component props in IDE
|
|
15
|
+
- Runtime errors from unexpected prop types
|
|
16
|
+
- `Property does not exist on type` errors
|
|
17
|
+
- **causes**:
|
|
18
|
+
- Component created without explicit props type
|
|
19
|
+
- Using `React.FC` without generic parameter
|
|
20
|
+
- Props spread without type narrowing
|
|
21
|
+
- **strategy**:
|
|
22
|
+
1. Define explicit interface for all props: `interface ButtonProps { ... }`
|
|
23
|
+
2. Use discriminated unions for variant props
|
|
24
|
+
3. Extend native HTML element props: `interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>`
|
|
25
|
+
4. Export props interface for consumers
|
|
26
|
+
- **tools**: file_read, file_edit
|
|
27
|
+
- **pitfalls**:
|
|
28
|
+
- Avoid `React.FC` -- it implicitly includes `children` in older React types and has issues with generics
|
|
29
|
+
- Do NOT use `PropsWithChildren` unless the component truly accepts arbitrary children
|
|
30
|
+
|
|
31
|
+
### 2. Prop Drilling
|
|
32
|
+
- **symptoms**:
|
|
33
|
+
- Same prop passed through 3+ component levels
|
|
34
|
+
- Intermediate components accept props they do not use
|
|
35
|
+
- Changing a prop signature requires editing many files
|
|
36
|
+
- `...rest` spread used to forward unknown props
|
|
37
|
+
- **causes**:
|
|
38
|
+
- Flat component hierarchy without composition
|
|
39
|
+
- Missing context or state management for shared data
|
|
40
|
+
- Over-reliance on top-down data flow
|
|
41
|
+
- **strategy**:
|
|
42
|
+
1. Use compound component pattern with React context
|
|
43
|
+
2. Use composition (children/render props) to skip levels
|
|
44
|
+
3. Introduce React Context for widely-shared state
|
|
45
|
+
4. Use Zustand/Jotai for cross-cutting application state
|
|
46
|
+
- **tools**: file_read, grep, file_edit
|
|
47
|
+
- **pitfalls**:
|
|
48
|
+
- Do NOT create a context for every piece of state -- only for truly cross-cutting concerns
|
|
49
|
+
- Context causes re-renders of all consumers -- split contexts by update frequency
|
|
50
|
+
|
|
51
|
+
### 3. Missing forwardRef
|
|
52
|
+
- **symptoms**:
|
|
53
|
+
- `ref` prop not working on custom component
|
|
54
|
+
- Warning: "Function components cannot be given refs"
|
|
55
|
+
- Parent cannot access child DOM node for focus management
|
|
56
|
+
- Animation libraries cannot attach to component
|
|
57
|
+
- **causes**:
|
|
58
|
+
- Custom component does not use `forwardRef`
|
|
59
|
+
- ref attached to wrong inner element
|
|
60
|
+
- Generic component loses type information for ref
|
|
61
|
+
- **strategy**:
|
|
62
|
+
1. Wrap with `React.forwardRef<HTMLElement, Props>((props, ref) => ...)`
|
|
63
|
+
2. Attach ref to the outermost meaningful DOM element
|
|
64
|
+
3. Use `useImperativeHandle` to expose a custom API
|
|
65
|
+
4. For generic components, use the ref-forwarding generic pattern
|
|
66
|
+
- **tools**: file_read, file_edit
|
|
67
|
+
- **pitfalls**:
|
|
68
|
+
- In React 19+, `ref` is a regular prop -- `forwardRef` will be unnecessary
|
|
69
|
+
- `forwardRef` breaks generic inference -- use the function overload pattern for generic components
|
|
70
|
+
|
|
71
|
+
### 4. Controlled vs Uncontrolled Confusion
|
|
72
|
+
- **symptoms**:
|
|
73
|
+
- "A component is changing an uncontrolled input to be controlled"
|
|
74
|
+
- Input value not updating on change
|
|
75
|
+
- Form reset not working
|
|
76
|
+
- Default value ignored after mount
|
|
77
|
+
- **causes**:
|
|
78
|
+
- Switching between `value` and `defaultValue`
|
|
79
|
+
- Initial `value` is `undefined` (uncontrolled) then becomes defined (controlled)
|
|
80
|
+
- Missing `onChange` handler with `value` prop
|
|
81
|
+
- **strategy**:
|
|
82
|
+
1. Choose controlled or uncontrolled and be consistent
|
|
83
|
+
2. For controlled: always pair `value` + `onChange`
|
|
84
|
+
3. For uncontrolled: use `defaultValue` + `ref`
|
|
85
|
+
4. Initialize state to empty string, not `undefined`: `useState('')`
|
|
86
|
+
5. Build components that support both modes with internal state fallback
|
|
87
|
+
- **tools**: file_read, file_edit
|
|
88
|
+
- **pitfalls**:
|
|
89
|
+
- `value={undefined}` makes it uncontrolled -- use `value={state ?? ''}` to stay controlled
|
|
90
|
+
- `defaultValue` is only read on mount -- changes after mount are ignored
|
|
91
|
+
|
|
92
|
+
### 5. Accessibility Violations
|
|
93
|
+
- **symptoms**:
|
|
94
|
+
- No keyboard navigation
|
|
95
|
+
- Screen reader cannot identify interactive elements
|
|
96
|
+
- Missing ARIA labels
|
|
97
|
+
- Focus trap not working in modals
|
|
98
|
+
- Color contrast failures
|
|
99
|
+
- **causes**:
|
|
100
|
+
- Using `div` with `onClick` instead of `button`
|
|
101
|
+
- Missing `aria-label` on icon buttons
|
|
102
|
+
- Custom components not forwarding ARIA props
|
|
103
|
+
- Missing focus management in dialogs
|
|
104
|
+
- **strategy**:
|
|
105
|
+
1. Use semantic HTML elements (`button`, `nav`, `main`, `dialog`)
|
|
106
|
+
2. Add `aria-label` to elements without visible text
|
|
107
|
+
3. Implement keyboard handlers: `onKeyDown` for Enter/Space/Escape
|
|
108
|
+
4. Use `role` attribute only when no semantic element exists
|
|
109
|
+
5. Trap focus in modals with `inert` attribute or focus-trap library
|
|
110
|
+
- **tools**: file_read, file_edit, shell_exec
|
|
111
|
+
- **pitfalls**:
|
|
112
|
+
- Do NOT add `role="button"` to a `div` when you can just use `<button>`
|
|
113
|
+
- `tabIndex={0}` alone is not enough -- also need keyboard event handlers
|
|
114
|
+
|
|
115
|
+
## Component Patterns
|
|
116
|
+
|
|
117
|
+
### Atomic Design Hierarchy
|
|
118
|
+
```
|
|
119
|
+
atoms/ -- Button, Input, Icon, Text, Badge
|
|
120
|
+
molecules/ -- SearchInput, FormField, Card, MenuItem
|
|
121
|
+
organisms/ -- Header, Sidebar, DataTable, Form
|
|
122
|
+
templates/ -- DashboardLayout, AuthLayout
|
|
123
|
+
pages/ -- HomePage, SettingsPage
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Compound Component Pattern
|
|
127
|
+
```
|
|
128
|
+
<Select>
|
|
129
|
+
<Select.Trigger />
|
|
130
|
+
<Select.Content>
|
|
131
|
+
<Select.Item value="a">Option A</Select.Item>
|
|
132
|
+
<Select.Item value="b">Option B</Select.Item>
|
|
133
|
+
</Select.Content>
|
|
134
|
+
</Select>
|
|
135
|
+
```
|
|
136
|
+
Uses React Context internally to share state between parent and children.
|
|
137
|
+
|
|
138
|
+
### Render Props / Children as Function
|
|
139
|
+
```
|
|
140
|
+
<DataFetcher url="/api/users">
|
|
141
|
+
{({ data, loading, error }) => (
|
|
142
|
+
loading ? <Spinner /> : <UserList users={data} />
|
|
143
|
+
)}
|
|
144
|
+
</DataFetcher>
|
|
145
|
+
```
|
|
146
|
+
Useful for separating data logic from presentation.
|
|
147
|
+
|
|
148
|
+
### Controlled + Uncontrolled Dual Mode
|
|
149
|
+
```
|
|
150
|
+
function Input({ value: controlledValue, defaultValue, onChange, ...props }) {
|
|
151
|
+
const [internalValue, setInternalValue] = useState(defaultValue ?? '');
|
|
152
|
+
const isControlled = controlledValue !== undefined;
|
|
153
|
+
const value = isControlled ? controlledValue : internalValue;
|
|
154
|
+
|
|
155
|
+
const handleChange = (e) => {
|
|
156
|
+
if (!isControlled) setInternalValue(e.target.value);
|
|
157
|
+
onChange?.(e);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return <input value={value} onChange={handleChange} {...props} />;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### forwardRef + Generic Component
|
|
165
|
+
```
|
|
166
|
+
interface ListProps<T> {
|
|
167
|
+
items: T[];
|
|
168
|
+
renderItem: (item: T) => ReactNode;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function ListInner<T>(props: ListProps<T>, ref: React.ForwardedRef<HTMLUListElement>) {
|
|
172
|
+
return (
|
|
173
|
+
<ul ref={ref}>
|
|
174
|
+
{props.items.map((item, i) => (
|
|
175
|
+
<li key={i}>{props.renderItem(item)}</li>
|
|
176
|
+
))}
|
|
177
|
+
</ul>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const List = React.forwardRef(ListInner) as <T>(
|
|
182
|
+
props: ListProps<T> & React.RefAttributes<HTMLUListElement>
|
|
183
|
+
) => ReactElement;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Tool Sequence
|
|
187
|
+
1. **file_read** -- Read existing component files to understand project patterns and conventions
|
|
188
|
+
2. **grep** -- Search for import patterns, style approach (CSS modules, Tailwind, styled-components)
|
|
189
|
+
3. **grep** -- Search for existing similar components to maintain consistency
|
|
190
|
+
4. **file_edit** -- Create the component file with proper TypeScript types, props interface, and JSDoc
|
|
191
|
+
5. **file_edit** -- Create barrel export (update `index.ts`)
|
|
192
|
+
6. **file_edit** -- Create test file stub with basic render test
|
|
193
|
+
7. **shell_exec** -- Run `tsc --noEmit` to verify types
|
|
194
|
+
8. **shell_exec** -- Run `pnpm build` to verify integration
|
|
195
|
+
|
|
196
|
+
## Validation Checklist
|
|
197
|
+
- [ ] Props interface is exported and fully typed (no `any`)
|
|
198
|
+
- [ ] Component has JSDoc description
|
|
199
|
+
- [ ] Default props use parameter defaults, not `defaultProps`
|
|
200
|
+
- [ ] `forwardRef` is used if component wraps a DOM element
|
|
201
|
+
- [ ] Semantic HTML elements used (not div-soup)
|
|
202
|
+
- [ ] ARIA attributes present for interactive elements
|
|
203
|
+
- [ ] Component handles all required variants/states
|
|
204
|
+
- [ ] Barrel export updated (`index.ts`)
|
|
205
|
+
- [ ] `tsc --noEmit` passes
|
|
206
|
+
- [ ] `pnpm build` passes
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# React Hook Extraction Skill
|
|
2
|
+
|
|
3
|
+
## Identity
|
|
4
|
+
- domain: react
|
|
5
|
+
- type: refactor
|
|
6
|
+
- confidence: 0.80
|
|
7
|
+
- persona: Senior React developer specializing in hook architecture, composition patterns, and separation of concerns. Expert in identifying extractable logic, creating testable custom hooks, and maintaining clean component boundaries.
|
|
8
|
+
|
|
9
|
+
## Known Error Patterns
|
|
10
|
+
|
|
11
|
+
### 1. God Component (Too Much Logic)
|
|
12
|
+
- **symptoms**:
|
|
13
|
+
- Component file exceeds 300 lines
|
|
14
|
+
- Multiple unrelated `useState`/`useEffect` pairs in one component
|
|
15
|
+
- Difficult to test individual behaviors
|
|
16
|
+
- Multiple concerns mixed (data fetching, form handling, animations)
|
|
17
|
+
- Code duplication across components for similar logic
|
|
18
|
+
- **causes**:
|
|
19
|
+
- Organic growth without refactoring
|
|
20
|
+
- All logic added directly to the component instead of hooks
|
|
21
|
+
- No separation between presentation and business logic
|
|
22
|
+
- **strategy**:
|
|
23
|
+
1. Identify logical groups: state + effects that belong together
|
|
24
|
+
2. Extract each group into a named custom hook: `useFormValidation`, `useDataFetch`
|
|
25
|
+
3. Component becomes a thin shell: hooks + JSX
|
|
26
|
+
4. Each hook should have a single responsibility
|
|
27
|
+
5. Hooks can compose other hooks
|
|
28
|
+
- **tools**: file_read, file_edit, grep
|
|
29
|
+
- **pitfalls**:
|
|
30
|
+
- Do NOT extract a hook with 10+ return values -- split further
|
|
31
|
+
- Do NOT create `useComponent` hooks that mirror the component 1:1 -- find meaningful abstractions
|
|
32
|
+
- Hooks should be reusable, not just "code moved to another file"
|
|
33
|
+
|
|
34
|
+
### 2. Duplicated State Logic
|
|
35
|
+
- **symptoms**:
|
|
36
|
+
- Same `useState` + `useEffect` pattern repeated in 3+ components
|
|
37
|
+
- Copy-paste of fetch/loading/error state management
|
|
38
|
+
- Identical form validation logic in multiple forms
|
|
39
|
+
- Similar timer/interval patterns across components
|
|
40
|
+
- **causes**:
|
|
41
|
+
- No shared hook library in the project
|
|
42
|
+
- Developers unaware of existing hooks
|
|
43
|
+
- Similar but slightly different requirements leading to copy-paste
|
|
44
|
+
- **strategy**:
|
|
45
|
+
1. Grep for repeated patterns: `useState.*loading`, `useEffect.*fetch`
|
|
46
|
+
2. Create a generalized hook with configuration parameters
|
|
47
|
+
3. Use TypeScript generics for type-safe data hooks
|
|
48
|
+
4. Document the hook's API and add it to a shared hooks directory
|
|
49
|
+
5. Replace all duplicated instances with the new hook
|
|
50
|
+
- **tools**: grep, file_read, file_edit
|
|
51
|
+
- **pitfalls**:
|
|
52
|
+
- Do NOT over-generalize -- if two patterns share 60% but differ in critical ways, two separate hooks may be better
|
|
53
|
+
- Ensure the extracted hook handles all edge cases from every call site
|
|
54
|
+
|
|
55
|
+
### 3. Tightly Coupled Effects
|
|
56
|
+
- **symptoms**:
|
|
57
|
+
- One `useEffect` does multiple unrelated things
|
|
58
|
+
- Dependency array contains unrelated values
|
|
59
|
+
- Changing one behavior requires modifying an unrelated effect
|
|
60
|
+
- Difficult to reason about when effects run
|
|
61
|
+
- **causes**:
|
|
62
|
+
- Multiple concerns combined in one effect for convenience
|
|
63
|
+
- "Just add it to the existing effect" mentality
|
|
64
|
+
- Unclear mental model of effect lifecycle
|
|
65
|
+
- **strategy**:
|
|
66
|
+
1. Split into separate `useEffect` calls -- one per concern
|
|
67
|
+
2. Group related state and effect into a custom hook
|
|
68
|
+
3. Each effect should have a clear, single purpose
|
|
69
|
+
4. Name the extracted hook after its purpose, not its implementation
|
|
70
|
+
- **tools**: file_read, file_edit
|
|
71
|
+
- **pitfalls**:
|
|
72
|
+
- Multiple effects are fine -- React handles them well
|
|
73
|
+
- Do NOT merge unrelated effects just to "reduce hook calls"
|
|
74
|
+
- Watch for effects that depend on each other -- these might need `useReducer` instead
|
|
75
|
+
|
|
76
|
+
### 4. Untestable Component Logic
|
|
77
|
+
- **symptoms**:
|
|
78
|
+
- Testing requires full component render for logic-only assertions
|
|
79
|
+
- Mocking is excessive because logic is interleaved with rendering
|
|
80
|
+
- Cannot test edge cases without UI interaction
|
|
81
|
+
- Business logic cannot be reused in different UI contexts
|
|
82
|
+
- **causes**:
|
|
83
|
+
- Business logic lives inside the component body
|
|
84
|
+
- Data transformations done inline in JSX
|
|
85
|
+
- Side effects triggered by rendering, not by explicit calls
|
|
86
|
+
- **strategy**:
|
|
87
|
+
1. Extract logic into a custom hook
|
|
88
|
+
2. Test the hook with `renderHook` from `@testing-library/react`
|
|
89
|
+
3. Hook returns a clean API: `{ data, loading, error, actions }`
|
|
90
|
+
4. Component becomes a thin rendering layer -- easy to test separately
|
|
91
|
+
5. Hook can be reused in different UI contexts (mobile, desktop, CLI)
|
|
92
|
+
- **tools**: file_read, file_edit, shell_exec
|
|
93
|
+
- **pitfalls**:
|
|
94
|
+
- `renderHook` is the correct way to test hooks -- do NOT call hooks outside React
|
|
95
|
+
- If a hook needs complex setup, consider if it is doing too much
|
|
96
|
+
|
|
97
|
+
### 5. Hook Composition Failure
|
|
98
|
+
- **symptoms**:
|
|
99
|
+
- Custom hook re-implements logic that exists in another hook
|
|
100
|
+
- Deep hook call chains that are hard to debug
|
|
101
|
+
- Hooks with too many parameters (> 4)
|
|
102
|
+
- Return type is a large tuple or object with 8+ fields
|
|
103
|
+
- **causes**:
|
|
104
|
+
- Not composing existing hooks
|
|
105
|
+
- Trying to make one hook do everything
|
|
106
|
+
- Flat hook architecture instead of layered
|
|
107
|
+
- **strategy**:
|
|
108
|
+
1. Build hooks in layers: primitive hooks -> domain hooks -> feature hooks
|
|
109
|
+
2. Primitive: `useLocalStorage`, `useDebounce`, `useMediaQuery`
|
|
110
|
+
3. Domain: `useAuth` (uses `useLocalStorage`), `useApi` (uses `useDebounce`)
|
|
111
|
+
4. Feature: `useUserSearch` (uses `useAuth` + `useApi` + `useDebounce`)
|
|
112
|
+
5. Each layer adds specific domain knowledge
|
|
113
|
+
- **tools**: file_read, grep, file_edit
|
|
114
|
+
- **pitfalls**:
|
|
115
|
+
- Avoid circular hook dependencies
|
|
116
|
+
- Keep primitive hooks truly primitive -- no business logic
|
|
117
|
+
- Document the hook layer architecture for the team
|
|
118
|
+
|
|
119
|
+
## Extraction Rules
|
|
120
|
+
|
|
121
|
+
### When to Extract
|
|
122
|
+
- Same logic appears in 2+ components
|
|
123
|
+
- Component has 3+ `useState` calls for unrelated concerns
|
|
124
|
+
- Component file exceeds 200 lines of logic (excluding JSX)
|
|
125
|
+
- An `useEffect` has 4+ dependencies from different concerns
|
|
126
|
+
- You need to test business logic independently from rendering
|
|
127
|
+
|
|
128
|
+
### When NOT to Extract
|
|
129
|
+
- Logic is used in exactly one component and is simple (< 20 lines)
|
|
130
|
+
- Extraction would create a hook with only one `useState` and no effects
|
|
131
|
+
- The "hook" would just be a wrapper around a single function call
|
|
132
|
+
- Logic is purely presentational (use a utility function instead)
|
|
133
|
+
|
|
134
|
+
### Naming Conventions
|
|
135
|
+
- `use<Resource><Action>`: `useUserFetch`, `useFormValidation`
|
|
136
|
+
- NOT `use<Component>Logic`: avoid `useHeaderLogic`, `useSidebarStuff`
|
|
137
|
+
- Return object, not tuple, if > 2 values: `{ data, loading, error, refetch }`
|
|
138
|
+
- File name matches hook name: `useUserFetch.ts`
|
|
139
|
+
|
|
140
|
+
### State + Effect Grouping
|
|
141
|
+
Group these together in one hook:
|
|
142
|
+
1. Related `useState` calls (e.g., `data`, `loading`, `error`)
|
|
143
|
+
2. The `useEffect` that manages them (e.g., fetch call)
|
|
144
|
+
3. Derived state (`useMemo`) from those states
|
|
145
|
+
4. Event handlers (`useCallback`) that modify those states
|
|
146
|
+
|
|
147
|
+
### Hook Composition Pattern
|
|
148
|
+
```
|
|
149
|
+
// Layer 1: Primitive
|
|
150
|
+
function useDebounce<T>(value: T, delay: number): T { ... }
|
|
151
|
+
|
|
152
|
+
// Layer 2: Domain
|
|
153
|
+
function useSearchApi(query: string) {
|
|
154
|
+
const debouncedQuery = useDebounce(query, 300);
|
|
155
|
+
const [results, setResults] = useState([]);
|
|
156
|
+
useEffect(() => { /* fetch with debouncedQuery */ }, [debouncedQuery]);
|
|
157
|
+
return { results };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Layer 3: Feature
|
|
161
|
+
function useUserSearch() {
|
|
162
|
+
const [query, setQuery] = useState('');
|
|
163
|
+
const { results } = useSearchApi(query);
|
|
164
|
+
const filtered = useMemo(() => filterActive(results), [results]);
|
|
165
|
+
return { query, setQuery, results: filtered };
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Testing Custom Hooks
|
|
170
|
+
```
|
|
171
|
+
import { renderHook, act } from '@testing-library/react';
|
|
172
|
+
import { useCounter } from './useCounter';
|
|
173
|
+
|
|
174
|
+
test('should increment counter', () => {
|
|
175
|
+
const { result } = renderHook(() => useCounter(0));
|
|
176
|
+
act(() => { result.current.increment(); });
|
|
177
|
+
expect(result.current.count).toBe(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('should handle async operations', async () => {
|
|
181
|
+
const { result } = renderHook(() => useDataFetch('/api/users'));
|
|
182
|
+
expect(result.current.loading).toBe(true);
|
|
183
|
+
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
184
|
+
expect(result.current.data).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Tool Sequence
|
|
189
|
+
1. **file_read** -- Read the target component to identify extractable logic groups
|
|
190
|
+
2. **grep** -- Search for similar patterns across the codebase to identify reuse opportunities
|
|
191
|
+
3. **grep** -- Check for existing hooks in `hooks/` or `utils/` that could be composed
|
|
192
|
+
4. **file_edit** -- Create the new custom hook file with full TypeScript types
|
|
193
|
+
5. **file_edit** -- Refactor the component to use the extracted hook
|
|
194
|
+
6. **file_edit** -- Create test file for the hook using `renderHook`
|
|
195
|
+
7. **shell_exec** -- Run `tsc --noEmit` to verify types
|
|
196
|
+
8. **shell_exec** -- Run tests to verify behavior is preserved
|
|
197
|
+
|
|
198
|
+
## Validation Checklist
|
|
199
|
+
- [ ] Hook has a single, clear responsibility
|
|
200
|
+
- [ ] Hook name follows `use<Resource><Action>` convention
|
|
201
|
+
- [ ] Return type is a typed object (not a large tuple)
|
|
202
|
+
- [ ] Hook is exported and documented with JSDoc
|
|
203
|
+
- [ ] Component is simpler after extraction (fewer lines, fewer concerns)
|
|
204
|
+
- [ ] All existing behavior is preserved (no regression)
|
|
205
|
+
- [ ] Hook test file exists with `renderHook` tests
|
|
206
|
+
- [ ] `tsc --noEmit` passes
|
|
207
|
+
- [ ] `pnpm build` passes
|
|
208
|
+
- [ ] No duplicated logic remains in the original component
|