ima-claude 2.9.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 +463 -0
- package/dist/cli.js +1064 -0
- package/package.json +49 -0
- package/platforms/claude/adapter.ts +115 -0
- package/platforms/junie/adapter.ts +254 -0
- package/platforms/junie/agents-template.md +113 -0
- package/platforms/junie/hook-translations.md +84 -0
- package/platforms/shared/detector.ts +27 -0
- package/platforms/shared/installer.ts +202 -0
- package/platforms/shared/types.ts +78 -0
- package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
- package/plugins/ima-claude/agents/explorer.md +30 -0
- package/plugins/ima-claude/agents/implementer.md +30 -0
- package/plugins/ima-claude/agents/memory.md +42 -0
- package/plugins/ima-claude/agents/reviewer.md +53 -0
- package/plugins/ima-claude/agents/tester.md +33 -0
- package/plugins/ima-claude/agents/wp-developer.md +46 -0
- package/plugins/ima-claude/hooks/README.md +145 -0
- package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
- package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
- package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
- package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
- package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
- package/plugins/ima-claude/hooks/docs_organization.py +104 -0
- package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
- package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
- package/plugins/ima-claude/hooks/hook_logger.py +69 -0
- package/plugins/ima-claude/hooks/hooks.json +239 -0
- package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
- package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
- package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
- package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
- package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
- package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
- package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
- package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
- package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
- package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
- package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
- package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
- package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
- package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
- package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
- package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
- package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
- package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
- package/plugins/ima-claude/personalities/README.md +45 -0
- package/plugins/ima-claude/personalities/enable-40k.md +69 -0
- package/plugins/ima-claude/personalities/enable-templars.md +69 -0
- package/plugins/ima-claude/skills/.research-summary.md +340 -0
- package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
- package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
- package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
- package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
- package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
- package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
- package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
- package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
- package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
- package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
- package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
- package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
- package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
- package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
- package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
- package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
- package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
- package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
- package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
- package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
- package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
- package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
- package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
- package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
- package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
- package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
- package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
- package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
- package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
- package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
- package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
- package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
- package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
- package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
- package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
- package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
- package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
- package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
- package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
- package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
- package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
- package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
- package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
- package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
- package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
- package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
- package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
- package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
- package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
- package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
- package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
- package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
- package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
- package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
- package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
- package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
- package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
- package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
- package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
- package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
- package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
- package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
- package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
- package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
- package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
- package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
- package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
- package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
- package/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh +61 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "js-fp-react"
|
|
3
|
+
description: "FP patterns for React with hooks, HOCs, and pure components - references js-fp core"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# JavaScript FP - React
|
|
7
|
+
|
|
8
|
+
Functional programming patterns for React components with custom hooks, higher-order components, and pure component architecture.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- Building React 16.8+ components (hooks era)
|
|
13
|
+
- Need pure, testable component logic
|
|
14
|
+
- Implementing custom hooks with FP principles
|
|
15
|
+
- HOC patterns for dependency injection
|
|
16
|
+
- Performance optimization without over-engineering
|
|
17
|
+
|
|
18
|
+
## Core Philosophy
|
|
19
|
+
|
|
20
|
+
**Pure components** with **business logic in custom hooks**, **HOCs for dependency injection**, and **appropriate memoization** (not obsessive).
|
|
21
|
+
|
|
22
|
+
**Foundation**: This skill builds on `js-fp` core principles. Reference `../js-fp/SKILL.md` for purity, composition, dependency injection, and testing patterns.
|
|
23
|
+
|
|
24
|
+
## Pure Component with Custom Hook Pattern
|
|
25
|
+
|
|
26
|
+
**Rule**: Separate business logic (custom hooks) from presentation (components).
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { memo, useMemo, useCallback } from 'react'
|
|
30
|
+
|
|
31
|
+
interface UserData {
|
|
32
|
+
id: string
|
|
33
|
+
name: string
|
|
34
|
+
email: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface UserConfig {
|
|
38
|
+
showEmail: boolean
|
|
39
|
+
variant: 'compact' | 'detailed'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ───── Custom hook with pure business logic ─────
|
|
43
|
+
const useUserLogic = (userData: UserData, config: UserConfig) => {
|
|
44
|
+
// Pure computation - no side effects
|
|
45
|
+
const displayData = useMemo(() => ({
|
|
46
|
+
...userData,
|
|
47
|
+
displayName: userData.name.trim(),
|
|
48
|
+
shouldShowEmail: config.showEmail && userData.email
|
|
49
|
+
}), [userData, config])
|
|
50
|
+
|
|
51
|
+
// Pre-compiled handlers
|
|
52
|
+
const handleAction = useCallback((action: string) => ({
|
|
53
|
+
type: 'USER_ACTION',
|
|
54
|
+
payload: { userId: userData.id, action }
|
|
55
|
+
}), [userData.id])
|
|
56
|
+
|
|
57
|
+
return { displayData, handleAction }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ───── Pure component with memo ─────
|
|
61
|
+
const UserCard = memo<UserCardProps>(({ userData, config, onAction }) => {
|
|
62
|
+
const { displayData, handleAction } = useUserLogic(userData, config)
|
|
63
|
+
|
|
64
|
+
const handleClick = useCallback(() => {
|
|
65
|
+
const action = handleAction('view')
|
|
66
|
+
onAction?.(action)
|
|
67
|
+
}, [handleAction, onAction])
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className={`user-card user-card--${config.variant}`}>
|
|
71
|
+
<h3>{displayData.displayName}</h3>
|
|
72
|
+
{displayData.shouldShowEmail && <p>{displayData.email}</p>}
|
|
73
|
+
<button onClick={handleClick}>View</button>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
UserCard.displayName = 'UserCard'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## HOC for Dependency Injection Pattern
|
|
82
|
+
|
|
83
|
+
**Rule**: Inject dependencies via HOCs for testability.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// ───── Service interfaces ─────
|
|
87
|
+
interface ServiceDependencies {
|
|
88
|
+
userService: {
|
|
89
|
+
getUser: (id: string) => Promise<UserData>
|
|
90
|
+
updateUser: (id: string, data: Partial<UserData>) => Promise<UserData>
|
|
91
|
+
}
|
|
92
|
+
logger: {
|
|
93
|
+
info: (message: string, meta?: any) => void
|
|
94
|
+
error: (message: string, meta?: any) => void
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ───── HOC factory (core: function factories) ─────
|
|
99
|
+
export const withUserService = <P extends object>(
|
|
100
|
+
WrappedComponent: React.ComponentType<P & ServiceDependencies>
|
|
101
|
+
) => {
|
|
102
|
+
const WithUserServiceComponent = (props: P) => {
|
|
103
|
+
// Service injection - can be mocked for testing
|
|
104
|
+
const services: ServiceDependencies = {
|
|
105
|
+
userService: useUserService(),
|
|
106
|
+
logger: useLogger()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return <WrappedComponent {...props} {...services} />
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
WithUserServiceComponent.displayName =
|
|
113
|
+
`withUserService(${WrappedComponent.displayName || WrappedComponent.name})`
|
|
114
|
+
|
|
115
|
+
return WithUserServiceComponent
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ───── Pure component with injected dependencies ─────
|
|
119
|
+
const UserProfile = ({
|
|
120
|
+
userId,
|
|
121
|
+
userService,
|
|
122
|
+
logger
|
|
123
|
+
}: { userId: string } & ServiceDependencies) => {
|
|
124
|
+
const [user, setUser] = useState<UserData | null>(null)
|
|
125
|
+
const [loading, setLoading] = useState(true)
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const loadUser = async () => {
|
|
129
|
+
try {
|
|
130
|
+
const userData = await userService.getUser(userId)
|
|
131
|
+
setUser(userData)
|
|
132
|
+
logger.info('User loaded', { userId })
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger.error('Failed to load user', { userId, error })
|
|
135
|
+
} finally {
|
|
136
|
+
setLoading(false)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
loadUser()
|
|
141
|
+
}, [userId, userService, logger])
|
|
142
|
+
|
|
143
|
+
if (loading) return <div>Loading...</div>
|
|
144
|
+
if (!user) return <div>User not found</div>
|
|
145
|
+
|
|
146
|
+
return <UserCard userData={user} config={{ showEmail: true, variant: 'detailed' }} />
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ───── Enhanced component with service injection ─────
|
|
150
|
+
export const UserProfileWithServices = withUserService(UserProfile)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Compound Component Pattern
|
|
154
|
+
|
|
155
|
+
**Rule**: Use composition for flexible, reusable component APIs.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { createContext, useContext, useState, useEffect, useCallback } from 'react'
|
|
159
|
+
|
|
160
|
+
// ───── Context for compound component ─────
|
|
161
|
+
interface ModalContextValue {
|
|
162
|
+
isOpen: boolean
|
|
163
|
+
onClose: () => void
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const ModalContext = createContext<ModalContextValue | null>(null)
|
|
167
|
+
|
|
168
|
+
const useModalContext = () => {
|
|
169
|
+
const context = useContext(ModalContext)
|
|
170
|
+
if (!context) {
|
|
171
|
+
throw new Error('Modal components must be used within Modal')
|
|
172
|
+
}
|
|
173
|
+
return context
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ───── Main compound component ─────
|
|
177
|
+
interface ModalProps {
|
|
178
|
+
isOpen: boolean
|
|
179
|
+
onClose: () => void
|
|
180
|
+
children: React.ReactNode
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const Modal = ({ isOpen, onClose, children }: ModalProps) => {
|
|
184
|
+
// Keyboard handling (side effect isolated)
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
187
|
+
if (event.key === 'Escape') onClose()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isOpen) {
|
|
191
|
+
document.addEventListener('keydown', handleEscape)
|
|
192
|
+
return () => document.removeEventListener('keydown', handleEscape)
|
|
193
|
+
}
|
|
194
|
+
}, [isOpen, onClose])
|
|
195
|
+
|
|
196
|
+
if (!isOpen) return null
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<ModalContext.Provider value={{ isOpen, onClose }}>
|
|
200
|
+
<div className="modal-overlay" onClick={onClose}>
|
|
201
|
+
<div className="modal-content" onClick={e => e.stopPropagation()}>
|
|
202
|
+
{children}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</ModalContext.Provider>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ───── Compound component parts ─────
|
|
210
|
+
Modal.Header = ({ children }: { children: React.ReactNode }) => {
|
|
211
|
+
const { onClose } = useModalContext()
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div className="modal-header">
|
|
215
|
+
{children}
|
|
216
|
+
<button onClick={onClose} aria-label="Close">×</button>
|
|
217
|
+
</div>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Modal.Body = ({ children }: { children: React.ReactNode }) => (
|
|
222
|
+
<div className="modal-body">{children}</div>
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
Modal.Footer = ({ children }: { children: React.ReactNode }) => (
|
|
226
|
+
<div className="modal-footer">{children}</div>
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
// ───── Usage - composition over configuration ─────
|
|
230
|
+
const UserEditModal = ({ isOpen, onClose, user }: {
|
|
231
|
+
isOpen: boolean
|
|
232
|
+
onClose: () => void
|
|
233
|
+
user: UserData
|
|
234
|
+
}) => (
|
|
235
|
+
<Modal isOpen={isOpen} onClose={onClose}>
|
|
236
|
+
<Modal.Header>Edit User: {user.name}</Modal.Header>
|
|
237
|
+
<Modal.Body>
|
|
238
|
+
<UserEditForm user={user} />
|
|
239
|
+
</Modal.Body>
|
|
240
|
+
<Modal.Footer>
|
|
241
|
+
<button onClick={onClose}>Cancel</button>
|
|
242
|
+
<button type="submit" form="user-edit-form">Save</button>
|
|
243
|
+
</Modal.Footer>
|
|
244
|
+
</Modal>
|
|
245
|
+
)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Performance Optimization (Evidence-Based)
|
|
249
|
+
|
|
250
|
+
**⚠️ IMPORTANT**: Follow core principles - optimize only when needed with evidence.
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// ✅ Good - memo for expensive renders
|
|
254
|
+
const ExpensiveComponent = memo(({ data }: { data: LargeDataSet }) => {
|
|
255
|
+
// Expensive rendering logic
|
|
256
|
+
return <ComplexVisualization data={data} />
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// ✅ Good - useMemo for expensive computations
|
|
260
|
+
const useExpensiveComputation = (largeDataSet: LargeDataSet) => {
|
|
261
|
+
const result = useMemo(() => {
|
|
262
|
+
return performExpensiveCalculation(largeDataSet) // Only when actually expensive
|
|
263
|
+
}, [largeDataSet])
|
|
264
|
+
|
|
265
|
+
return result
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ✅ Good - useCallback to prevent prop changes
|
|
269
|
+
const Parent = () => {
|
|
270
|
+
const [count, setCount] = useState(0)
|
|
271
|
+
|
|
272
|
+
const handleClick = useCallback(() => {
|
|
273
|
+
setCount(c => c + 1)
|
|
274
|
+
}, [])
|
|
275
|
+
|
|
276
|
+
return <MemoizedChild onCount={handleClick} />
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ❌ Avoid - unnecessary optimization
|
|
280
|
+
const SimpleComponent = memo(({ text }: { text: string }) => <p>{text}</p>) // Not needed
|
|
281
|
+
|
|
282
|
+
// ❌ Avoid - over-using useMemo
|
|
283
|
+
const DisplayName = ({ first, last }: { first: string; last: string }) => {
|
|
284
|
+
const fullName = useMemo(() => `${first} ${last}`, [first, last]) // Overkill
|
|
285
|
+
return <p>{fullName}</p>
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ✅ Better - direct computation for simple operations
|
|
289
|
+
const DisplayName = ({ first, last }: { first: string; last: string }) => {
|
|
290
|
+
const fullName = `${first} ${last}` // Simple, no memo needed
|
|
291
|
+
return <p>{fullName}</p>
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Testing React FP Components
|
|
296
|
+
|
|
297
|
+
### Test Custom Hooks
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// __tests__/useUserLogic.test.ts
|
|
301
|
+
import { renderHook } from '@testing-library/react-hooks'
|
|
302
|
+
import { useUserLogic } from '../useUserLogic'
|
|
303
|
+
|
|
304
|
+
describe('useUserLogic', () => {
|
|
305
|
+
it('processes user data correctly', () => {
|
|
306
|
+
const { result } = renderHook(() =>
|
|
307
|
+
useUserLogic(
|
|
308
|
+
{ id: '1', name: ' John ', email: 'john@test.com' },
|
|
309
|
+
{ showEmail: true, variant: 'compact' }
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
expect(result.current.displayData.displayName).toBe('John')
|
|
314
|
+
expect(result.current.displayData.email).toBe('john@test.com')
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('hides email when config.showEmail is false', () => {
|
|
318
|
+
const { result } = renderHook(() =>
|
|
319
|
+
useUserLogic(
|
|
320
|
+
{ id: '1', name: 'John', email: 'john@test.com' },
|
|
321
|
+
{ showEmail: false, variant: 'compact' }
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
expect(result.current.displayData.shouldShowEmail).toBe(false)
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Test Components with React Testing Library
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// __tests__/UserCard.test.tsx
|
|
334
|
+
import { render, screen, userEvent } from '@testing-library/react'
|
|
335
|
+
import UserCard from '../UserCard'
|
|
336
|
+
|
|
337
|
+
describe('UserCard', () => {
|
|
338
|
+
it('renders user data correctly', () => {
|
|
339
|
+
render(
|
|
340
|
+
<UserCard
|
|
341
|
+
userData={{ id: '1', name: 'John', email: 'john@test.com' }}
|
|
342
|
+
config={{ showEmail: true, variant: 'compact' }}
|
|
343
|
+
onAction={jest.fn()}
|
|
344
|
+
/>
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
expect(screen.getByText('John')).toBeInTheDocument()
|
|
348
|
+
expect(screen.getByText('john@test.com')).toBeInTheDocument()
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('calls onAction when button clicked', async () => {
|
|
352
|
+
const onAction = jest.fn()
|
|
353
|
+
render(
|
|
354
|
+
<UserCard
|
|
355
|
+
userData={{ id: '1', name: 'John', email: 'john@test.com' }}
|
|
356
|
+
config={{ showEmail: true, variant: 'compact' }}
|
|
357
|
+
onAction={onAction}
|
|
358
|
+
/>
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
await userEvent.click(screen.getByRole('button'))
|
|
362
|
+
expect(onAction).toHaveBeenCalled()
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Anti-Patterns (AVOID)
|
|
368
|
+
|
|
369
|
+
### ❌ Overusing Context
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// ❌ Context for local state
|
|
373
|
+
const UserContext = createContext<UserData | null>(null)
|
|
374
|
+
|
|
375
|
+
// ✅ Props for local state (simpler)
|
|
376
|
+
<UserCard userData={user} />
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### ❌ Premature Memoization
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// ❌ Memoizing everything
|
|
383
|
+
const Component = () => {
|
|
384
|
+
const a = useMemo(() => 1 + 1, []) // Overkill
|
|
385
|
+
const b = useMemo(() => 'hello', []) // Overkill
|
|
386
|
+
const c = useCallback(() => {}, []) // Overkill when passed to non-memoized children
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ✅ Memoize only when needed
|
|
390
|
+
const Component = () => {
|
|
391
|
+
const a = 2 // Simple calculation
|
|
392
|
+
const b = 'hello' // Simple value
|
|
393
|
+
const c = () => {} // Only memo if passed to memoized child
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Quality Gates
|
|
398
|
+
|
|
399
|
+
Before implementing any React component:
|
|
400
|
+
|
|
401
|
+
1. ✅ **Pure custom hook**: Business logic separated from presentation?
|
|
402
|
+
2. ✅ **HOC for DI**: Dependencies injected via HOC when appropriate?
|
|
403
|
+
3. ✅ **Appropriate memoization**: Using memo/useMemo/useCallback only when needed?
|
|
404
|
+
4. ✅ **Compound components**: Using composition for flexible APIs?
|
|
405
|
+
5. ✅ **Testability**: Can inject mocks for all dependencies?
|
|
406
|
+
6. ✅ **FP principles**: Pure functions, immutable updates?
|
|
407
|
+
7. ✅ **Performance**: Optimized without over-engineering?
|
|
408
|
+
|
|
409
|
+
## When to Load Additional Content
|
|
410
|
+
|
|
411
|
+
### Hooks Advanced
|
|
412
|
+
**File**: `references/hooks-advanced.md`
|
|
413
|
+
**When**: Complex custom hooks, advanced patterns
|
|
414
|
+
**Contains**: Hook composition, state machines, async patterns, effect isolation
|
|
415
|
+
|
|
416
|
+
### Performance Patterns
|
|
417
|
+
**File**: `references/performance-patterns.md`
|
|
418
|
+
**When**: Performance optimization needed, large lists
|
|
419
|
+
**Contains**: React.memo strategies, virtualization, code splitting
|
|
420
|
+
|
|
421
|
+
### Working Examples
|
|
422
|
+
**File**: `examples/ProductCard.tsx`
|
|
423
|
+
**When**: Need complete working component example
|
|
424
|
+
**Contains**: Full ProductCard component with custom hook, types, and exports
|
|
425
|
+
|
|
426
|
+
## Foundation Reference
|
|
427
|
+
|
|
428
|
+
**Core FP Principles**: `../js-fp/SKILL.md`
|
|
429
|
+
- Purity and side effect isolation
|
|
430
|
+
- Composition patterns
|
|
431
|
+
- Dependency injection
|
|
432
|
+
- Immutability
|
|
433
|
+
- Testing strategies
|
|
434
|
+
|
|
435
|
+
**Deep Dive**: `../js-fp/core-principles.md` for complete FP philosophy
|
|
436
|
+
|
|
437
|
+
## Success Metrics
|
|
438
|
+
|
|
439
|
+
- **Testability**: 100% testable custom hooks
|
|
440
|
+
- **Performance**: Appropriate memoization, sub-100ms renders
|
|
441
|
+
- **Maintainability**: Clear separation of concerns
|
|
442
|
+
- **Code Quality**: Simple, readable component logic
|
|
443
|
+
- **Bundle Size**: Tree-shakeable, minimal overhead
|
|
444
|
+
|
|
445
|
+
## Philosophy
|
|
446
|
+
|
|
447
|
+
*"Pure component architecture through custom hooks, HOCs for dependency injection, and appropriate memoization - optimize for testability and simplicity over premature optimization."*
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// ProductCard.tsx
|
|
2
|
+
// Complete FP React component example demonstrating:
|
|
3
|
+
// - Custom hook for business logic
|
|
4
|
+
// - Pure component with memo
|
|
5
|
+
// - Appropriate memoization
|
|
6
|
+
// - TypeScript interfaces
|
|
7
|
+
|
|
8
|
+
import { memo, useMemo, useCallback } from 'react'
|
|
9
|
+
|
|
10
|
+
// ───── Types ─────
|
|
11
|
+
interface Product {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
price: number
|
|
15
|
+
inStock: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ProductCardProps {
|
|
19
|
+
product: Product
|
|
20
|
+
onAddToCart: (productId: string) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ───── Custom Hook (Business Logic) ─────
|
|
24
|
+
// Pure computations separated from presentation
|
|
25
|
+
const useProductLogic = (product: Product) => {
|
|
26
|
+
const displayData = useMemo(() => ({
|
|
27
|
+
...product,
|
|
28
|
+
formattedPrice: `$${product.price.toFixed(2)}`,
|
|
29
|
+
availability: product.inStock ? 'In Stock' : 'Out of Stock'
|
|
30
|
+
}), [product])
|
|
31
|
+
|
|
32
|
+
const cssClasses = useMemo(() => ({
|
|
33
|
+
card: `product-card ${product.inStock ? 'in-stock' : 'out-of-stock'}`,
|
|
34
|
+
price: `product-price ${product.inStock ? 'available' : 'unavailable'}`
|
|
35
|
+
}), [product.inStock])
|
|
36
|
+
|
|
37
|
+
return { displayData, cssClasses }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ───── Pure Component ─────
|
|
41
|
+
const ProductCard = memo<ProductCardProps>(({ product, onAddToCart }) => {
|
|
42
|
+
const { displayData, cssClasses } = useProductLogic(product)
|
|
43
|
+
|
|
44
|
+
const handleAddToCart = useCallback(() => {
|
|
45
|
+
if (product.inStock) {
|
|
46
|
+
onAddToCart(product.id)
|
|
47
|
+
}
|
|
48
|
+
}, [product.inStock, product.id, onAddToCart])
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={cssClasses.card}>
|
|
52
|
+
<h3>{displayData.name}</h3>
|
|
53
|
+
<p className={cssClasses.price}>{displayData.formattedPrice}</p>
|
|
54
|
+
<p className="availability">{displayData.availability}</p>
|
|
55
|
+
<button onClick={handleAddToCart} disabled={!product.inStock}>
|
|
56
|
+
Add to Cart
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
ProductCard.displayName = 'ProductCard'
|
|
63
|
+
|
|
64
|
+
export { ProductCard, useProductLogic }
|
|
65
|
+
export type { Product, ProductCardProps }
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Advanced React Hooks Patterns
|
|
2
|
+
|
|
3
|
+
Advanced patterns for custom hooks following FP principles.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Composition Patterns](#composition-patterns)
|
|
8
|
+
2. [State Machine Hooks](#state-machine-hooks)
|
|
9
|
+
3. [Async Data Hooks](#async-data-hooks)
|
|
10
|
+
4. [Effect Isolation](#effect-isolation)
|
|
11
|
+
|
|
12
|
+
## Composition Patterns
|
|
13
|
+
|
|
14
|
+
### Hook Composition
|
|
15
|
+
|
|
16
|
+
Compose smaller hooks into larger ones:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Small, focused hooks
|
|
20
|
+
const useToggle = (initial = false) => {
|
|
21
|
+
const [value, setValue] = useState(initial)
|
|
22
|
+
const toggle = useCallback(() => setValue(v => !v), [])
|
|
23
|
+
return { value, toggle }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const useAsync = <T>(asyncFn: () => Promise<T>, deps: any[]) => {
|
|
27
|
+
const [state, setState] = useState<{
|
|
28
|
+
data: T | null; loading: boolean; error: Error | null
|
|
29
|
+
}>({ data: null, loading: true, error: null })
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setState(s => ({ ...s, loading: true }))
|
|
33
|
+
asyncFn()
|
|
34
|
+
.then(data => setState({ data, loading: false, error: null }))
|
|
35
|
+
.catch(error => setState({ data: null, loading: false, error }))
|
|
36
|
+
}, deps)
|
|
37
|
+
|
|
38
|
+
return state
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Reducer Pattern for Complex State
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
type Action =
|
|
46
|
+
| { type: 'FETCH_START' }
|
|
47
|
+
| { type: 'FETCH_SUCCESS'; payload: User[] }
|
|
48
|
+
| { type: 'FETCH_ERROR'; error: Error }
|
|
49
|
+
|
|
50
|
+
// Pure reducer function
|
|
51
|
+
const userReducer = (state: State, action: Action): State => {
|
|
52
|
+
switch (action.type) {
|
|
53
|
+
case 'FETCH_START': return { ...state, loading: true, error: null }
|
|
54
|
+
case 'FETCH_SUCCESS': return { ...state, users: action.payload, loading: false }
|
|
55
|
+
case 'FETCH_ERROR': return { ...state, error: action.error, loading: false }
|
|
56
|
+
default: return state
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## State Machine Hooks
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
type FormState = 'idle' | 'validating' | 'submitting' | 'success' | 'error'
|
|
65
|
+
|
|
66
|
+
const useFormMachine = () => {
|
|
67
|
+
const [state, setState] = useState<FormState>('idle')
|
|
68
|
+
|
|
69
|
+
const transition = useCallback((event: string) => {
|
|
70
|
+
setState(current => {
|
|
71
|
+
const transitions: Record<FormState, Record<string, FormState>> = {
|
|
72
|
+
idle: { SUBMIT: 'submitting' },
|
|
73
|
+
submitting: { SUCCESS: 'success', FAILURE: 'error' },
|
|
74
|
+
error: { RETRY: 'idle' },
|
|
75
|
+
success: { RESET: 'idle' }
|
|
76
|
+
}
|
|
77
|
+
return transitions[current]?.[event] ?? current
|
|
78
|
+
})
|
|
79
|
+
}, [])
|
|
80
|
+
|
|
81
|
+
return { state, canSubmit: state === 'idle', transition }
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Async Data Hooks
|
|
86
|
+
|
|
87
|
+
### Debounced Search
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const useDebouncedSearch = <T>(searchFn: (q: string) => Promise<T[]>, delay = 300) => {
|
|
91
|
+
const [query, setQuery] = useState('')
|
|
92
|
+
const [results, setResults] = useState<T[]>([])
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!query.trim()) { setResults([]); return }
|
|
96
|
+
const timer = setTimeout(async () => {
|
|
97
|
+
setResults(await searchFn(query))
|
|
98
|
+
}, delay)
|
|
99
|
+
return () => clearTimeout(timer)
|
|
100
|
+
}, [query, searchFn, delay])
|
|
101
|
+
|
|
102
|
+
return { query, setQuery, results }
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Effect Isolation
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// Pure computation hook
|
|
110
|
+
const useCartCalculations = (items: CartItem[]) => {
|
|
111
|
+
return useMemo(() => ({
|
|
112
|
+
subtotal: items.reduce((sum, item) => sum + item.price * item.qty, 0),
|
|
113
|
+
itemCount: items.reduce((sum, item) => sum + item.qty, 0)
|
|
114
|
+
}), [items])
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Side effect hook (separated)
|
|
118
|
+
const useCartPersistence = (items: CartItem[]) => {
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
localStorage.setItem('cart', JSON.stringify(items))
|
|
121
|
+
}, [items])
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Testing
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Test reducer in isolation (pure function)
|
|
129
|
+
describe('userReducer', () => {
|
|
130
|
+
it('handles FETCH_SUCCESS', () => {
|
|
131
|
+
const state = { users: [], loading: true, error: null }
|
|
132
|
+
const result = userReducer(state, { type: 'FETCH_SUCCESS', payload: [mockUser] })
|
|
133
|
+
expect(result.users).toEqual([mockUser])
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
```
|