From 5abf645bcd4e60edb5a58744f07984c4429dcc39 Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Fri, 3 Apr 2026 16:40:01 +0900 Subject: [PATCH 01/14] feat: add react-hook-philosophy plugin with design principles Add Claude Code plugin with 31 hook design principles (C1-C14 coding + U1-U17 usage patterns) for code review and writing guidance. Includes design document, React hook usage patterns reference, and 3 complete hook implementation examples. --- docs/hook-design-principles.md | 329 ++++++++++++++++++ docs/react-hook-usage-patterns.md | 164 +++++++++ packages/plugin/.claude-plugin/plugin.json | 10 + packages/plugin/README.md | 48 +++ .../plugin/skills/react-hook-review/SKILL.md | 101 ++++++ .../plugin/skills/react-hook-writing/SKILL.md | 122 +++++++ .../react-hook-writing/references/patterns.md | 234 +++++++++++++ 7 files changed, 1008 insertions(+) create mode 100644 docs/hook-design-principles.md create mode 100644 docs/react-hook-usage-patterns.md create mode 100644 packages/plugin/.claude-plugin/plugin.json create mode 100644 packages/plugin/README.md create mode 100644 packages/plugin/skills/react-hook-review/SKILL.md create mode 100644 packages/plugin/skills/react-hook-writing/SKILL.md create mode 100644 packages/plugin/skills/react-hook-writing/references/patterns.md diff --git a/docs/hook-design-principles.md b/docs/hook-design-principles.md new file mode 100644 index 00000000..e557b5e8 --- /dev/null +++ b/docs/hook-design-principles.md @@ -0,0 +1,329 @@ +# React Hook Design Principles + +> 최종 업데이트: 2026-04-03 +> 상태: Draft (논의 후 확정) + +--- + +## 1. 요구사항 + +### 배경 + +react-simplikit을 운영하며 축적한 훅 설계 철학을 **하나의 공통 원칙**으로 정의한다. 이 원칙은 두 가지 용도로 사용된다: + +1. **코드 리뷰** — `react-hook-review` 스킬이 이 원칙 기반으로 피드백 +2. **코드 작성** — `react-hook-writing` 스킬이 이 원칙 기반으로 가이드 + +### 원칙의 두 가지 방향 + +| 방향 | 출처 | 범위 | +|------|------|------| +| **훅 코딩 원칙** (Section 2) | CLAUDE.md, AGENTS.md, 내부 스킬 | 반환값, TypeScript, 성능, 문서화 등 코딩 스타일 | +| **훅 사용 패턴** (Section 3) | React 공식 문서 (react.dev) | state 설계, effect 사용법, 메모이제이션, 커스텀 훅 설계 | + +### 핵심 요구사항 + +| # | 요구사항 | 상세 | +|---|---------|------| +| R1 | 리뷰/생성 공통 원칙 | 두 스킬이 동일한 원칙 참조 | +| R2 | Why 중심 | 규칙(What)만 나열하지 않고 철학(Why)을 narrative로 설명 | +| R3 | Opinionated 투명성 | 🟢 Best Practice vs 🟡 Opinionated 명시 | +| R4 | 프로젝트 무관 | react-simplikit 경로/명령어/유틸 없이 범용 원칙만 | +| R5 | Cross-tool | Claude Code 플러그인 + Codex(AGENTS.md) + Cursor(.cursorrules) | + +### 결정 필요 사항 + +| # | 질문 | 선택지 | +|---|------|--------| +| Q1 | C14(Named useEffect)를 포함할지? | A) "Recommended"로 포함 B) 제외 | +| Q2 | C2(SSR-Safe)를 비-SSR 프로젝트에도 권장할지? | A) 항상 B) SSR 사용 시만 | +| Q3 | C9(JSDoc)의 @example을 필수로 할지? | A) 4-tag 전부 필수 B) @example은 권장 | +| Q4 | 추가할 원칙이 있는지? | — | +| Q5 | 원칙 먼저 확정할지, 바로 플러그인 구조로 갈지? | A) 원칙 먼저 B) 바로 플러그인 | +| Q6 | 플러그인 배포 채널 | A) git-subdir B) npm C) 미정 | + +--- + +## 2. 훅 코딩 원칙 (Direction 1) + +CLAUDE.md + AGENTS.md + 내부 스킬에서 추출한 **코딩 스타일** 원칙. + +### 🟢 Best Practice (13개) + +#### C1. 항상 객체 반환 + +반환값이 1개여도 `{ value }` 형태. 객체는 순서 무관, 이름으로 의미 전달, 확장 시 breaking change 없음. + +```ts +function useDebounce(value: T, delay: number): { value: T } +function useToggle(init: boolean): { value: boolean; toggle: () => void } +function usePagination(): { page: number; next: () => void; prev: () => void } +``` + +#### C2. SSR-Safe 초기화 + +`useState(FIXED_VALUE)` + `useEffect(sync)`. 브라우저 API 초기화 금지. 서버에 `window` 없음 → 크래시 또는 hydration mismatch. + +```ts +// ✅ SSR 안전 +const [width, setWidth] = useState(0); +useEffect(function syncWidth() { setWidth(window.innerWidth); }, []); + +// ❌ SSR 크래시 +const [width, setWidth] = useState(window.innerWidth); + +// ⚠️ 클라이언트 전용 앱에서만 허용 +const [width, setWidth] = useState(() => { + if (typeof window === 'undefined') return 0; + return window.innerWidth; +}); +``` + +#### C3. useEffect Cleanup 필수 + +모든 부수효과에 cleanup 반환. 메모리 누수 방지. StrictMode 이중 마운트가 즉시 노출. + +```ts +// 이벤트 리스너 +useEffect(function subscribe() { + window.addEventListener('resize', handler); + return () => window.removeEventListener('resize', handler); +}, []); + +// AbortController (비동기) +useEffect(function fetchData() { + const controller = new AbortController(); + fetch(url, { signal: controller.signal }).then(/* ... */); + return () => controller.abort(); +}, [url]); + +// 타이머 +useEffect(function tick() { + const id = setInterval(callback, 1000); + return () => clearInterval(id); +}, []); +``` + +#### C4. No `any` Types + +제네릭 `` 사용. any 전파 → 타입 시스템 무력화. 정당한 사유(generic callback 등) 시 per-line eslint-disable + 코멘트 허용. + +```ts +// ✅ Generic +function useDebounce(value: T, delay: number): T + +// ✅ 정당한 예외 (코멘트 필수) +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic callback +type AnyFunction = (...args: any[]) => unknown; +``` + +#### C5. Named Exports Only + +tree-shaking 보장 + import 명확성. `export default` 금지. + +#### C6. Strict Boolean & Nullish Checks + +`if (value)` 금지 → 0, "", false falsy 버그 방지. `== null`로 null+undefined 동시 체크. + +```ts +if (ref == null) { return; } // ✅ null + undefined +const controlled = valueProp !== undefined; // ✅ 구분 필요할 때 +if (count) { ... } // ❌ count=0 통과 못함 +``` + +#### C7. Parameter는 객체로 받기 + +훅의 인자를 개별 파라미터 대신 객체(props)로. 순서 무관 + 이름으로 의미 전달 + 확장 시 breaking change 없음. + +```ts +// ✅ 객체 +function useDebounce({ value, delay, leading }: { + value: T; delay: number; leading?: boolean; +}): { value: T } + +// ❌ 위치 기반 +function useDebounce(value: T, delay: number, leading?: boolean): { value: T } +``` + +#### C8. Guard Clauses (Early Return) + +nested if-else 대신 early return. 실패 조건 먼저 걸러내고 성공 로직은 플랫하게. + +```ts +// ✅ +function process(value: string | null) { + if (value == null) { return DEFAULT; } + return transform(value); +} + +// ❌ +function process(value: string | null) { + if (value != null) { return transform(value); } else { return DEFAULT; } +} +``` + +#### C9. JSDoc 4-Tag + +모든 public API에 `@description` + `@param` + `@returns` + `@example`. AI 문서 생성 + IDE 툴팁. + +```ts +/** + * @description 값의 변경을 지연시킨다. + * @param value - 디바운스할 값 + * @param delay - 지연 시간 (ms) + * @returns 디바운스된 값 + * @example + * const debouncedQuery = useDebounce(query, 300); + */ +``` + +#### C10. Performance Patterns + +고빈도(30+/sec) 이벤트에만 적용. 일반 훅에는 불필요. + +| 기법 | 적용 시점 | +|------|-----------| +| Throttle (16ms) | scroll, resize, pointer, keyboard | +| Deduplicate | 값 미변경 시 setState skip | +| startTransition | 비긴급 파생 계산 (React 18+) | + +#### C11. Function Keyword for Declarations + +함수 선언은 `function` 키워드. 화살표는 인라인 콜백(map, filter)에만. + +```ts +function toggle(state: boolean) { return !state; } // ✅ 선언 +items.filter(item => item != null); // ✅ 인라인 +const toggle = (state: boolean) => !state; // ❌ 선언에 화살표 +``` + +#### C12. Zero Runtime Dependencies + +프로덕션 코드에 외부 런타임 의존성 금지. `peerDependencies`만 허용. 번들 사이즈 최소화 + 의존성 충돌 방지. + +#### C13. 외부 의존성 직접 참조 지양 + +훅 내부에서 외부 모듈을 직접 호출하지 않고 인자로 주입. 테스트 용이성 + 교체 가능성. + +```ts +// ✅ 의존성 주입 +function useFetch(fetcher: (url: string) => Promise, url: string) { ... } + +// ❌ 외부 모듈 직접 +function useFetch(url: string) { const res = await axios.get(url); ... } +``` + +### 🟡 Opinionated (1개) + +#### C14. Named useEffect Functions + +`useEffect(function handleResize() {...})`. 에러 스택에서 "handleResize" vs "anonymous". Trade-off: 화살표보다 장황. cleanup 이름은 "Recommended" (필수 아님). + +### 제외 (프로젝트별 결정) + +| 항목 | 이유 | +|------|------| +| Import extensions (.js/.ts) | 빌드 도구 의존적 | +| 100% test coverage | 프로젝트 정책 | +| 파일 구조/커밋 컨벤션 | 훅 설계 철학 아님 | + +--- + +## 3. 훅 사용 패턴 (Direction 2) + +> 별도 문서: [react-hook-usage-patterns.md](./react-hook-usage-patterns.md) + +React 공식 문서(react.dev) 기반 17개 패턴 (U1-U17): + +| 카테고리 | 개수 | 핵심 | +|----------|------|------| +| State Design | U1-U7 | 파생값 계산, props 복사 금지, useRef, useReducer, union type | +| Effect Usage | U8-U14 | effect는 외부 동기화 전용, 체인 금지, key 리셋, 비동기 cleanup | +| Memoization | U15-U16 | useMemo 1ms+, useCallback + memo() 조합만 | +| Hook Design | U17 | lifecycle wrapper 금지, 구체적 목적 훅만 | + +--- + +## 4. 플러그인 아키텍처 + +### 파생 흐름 + +``` +이 문서 (principles, 원칙 정의) + ↓ 압축 +react-hook-review/SKILL.md (체크리스트) +react-hook-writing/SKILL.md (가이드) + ↓ 추가 압축 +AGENTS.md Part 1 (Codex용) + ↓ 참조 +.cursorrules (Cursor용) +``` + +### 디렉토리 구조 + +``` +packages/plugin/ (planned) +├── .claude-plugin/plugin.json +├── .codex-plugin/plugin.json +├── principles/ ← 공통 원칙 Single Source +├── skills/ +│ ├── react-hook-review/SKILL.md ← C1-C14 + U1-U17 체크리스트 +│ └── react-hook-writing/ +│ ├── SKILL.md ← 테마별 가이드 +│ └── references/patterns.md ← 구현 예시 3개 +└── README.md +``` + +### Cross-Tool 지원 + +| 도구 | 파일 | 현재 | 변경 | +|------|------|------|------| +| Claude Code (내부) | `.claude/skills/` | ✅ 10개 | 유지 | +| Claude Code (플러그인) | `packages/plugin/` | ❌ | Phase 1-5로 생성 | +| Codex | `AGENTS.md` | ✅ 162줄 | Part 1(Universal) + Part 2(Project) 분리 | +| Cursor | `.cursorrules` | ✅ 28줄 | AGENTS.md 참조 유지 | + +### 추출 규칙 + +| 추출됨 (철학) | 남겨짐 (구현) | +|--------------|-------------| +| "항상 객체 반환" | `packages/core/src/hooks/` 경로 | +| "Named useEffect improves stack traces" | `yarn test`, `yarn fix` 명령 | +| "SSR-safe: fixed initial + useEffect sync" | `renderHookSSR.serverOnly()` 유틸 | +| "4 JSDoc tags for AI doc generation" | `100%` coverage 기준 | + +### 일반화 변환 + +| Before (프로젝트 전용) | After (범용) | +|---|---| +| `renderHookSSR.serverOnly()` | Vitest + `delete global.window` | +| `yarn test` / `yarn fix` | "Run your test suite" | +| `packages/core/` 경로 | "your source directory" | +| `react-simplikit` 언급 | 제거 | + +--- + +## 5. 실행 로드맵 + +| Phase | 내용 | 산출물 | +|-------|------|--------| +| 1 | 디렉토리 + plugin.json + README | `packages/plugin/` 구조 | +| 2 | react-hook-review SKILL.md | C1-C14 + U1-U17 체크리스트 | +| 3 | react-hook-writing SKILL.md + patterns.md | 테마별 가이드 + 3개 훅 예시 | +| 4 | 일반화 검증 (grep) | 프로젝트 참조 0건 | +| 5 | 플러그인 validate + 로컬 테스트 | 동작 확인 | + +### 검증 기준 + +| 항목 | 통과 기준 | +|------|---------| +| 플러그인 구조 | `claude plugin validate .` 에러 0 | +| 범용성 | 다른 React 프로젝트에서 프로젝트 참조 0건 | +| 철학 깊이 | 각 규칙의 Why가 narrative | +| Opinionated 투명성 | 🟡 패턴에 trade-off 존재 | + +### 향후 확장 + +- Codex/Gemini 대응 (AGENTS.md Part 1 활용) +- Component 설계 철학 추가 +- Marketplace 전환 (Plugin 3개+ 시) diff --git a/docs/react-hook-usage-patterns.md b/docs/react-hook-usage-patterns.md new file mode 100644 index 00000000..67eae610 --- /dev/null +++ b/docs/react-hook-usage-patterns.md @@ -0,0 +1,164 @@ +# React Hook Usage Patterns + +> 최종 업데이트: 2026-04-03 +> 출처: React 공식 문서 (react.dev) +> 관련: [Hook Design Principles](./hook-design-principles.md) + +코딩 스타일이 아닌 **hooks를 올바르게 사용하는 패턴**. 17개 원칙. + +--- + +## State Design (7개) + +### U1. 파생 가능한 값은 state에 넣지 마라 + +기존 props/state에서 계산 가능한 값은 렌더 중에 계산. useEffect 동기화 → 1렌더 지연 + 불필요한 추가 렌더. + +```ts +// ❌ const [fullName, setFullName] = useState(''); +// useEffect(() => { setFullName(first + ' ' + last); }, [first, last]); +// ✅ const fullName = first + ' ' + last; +``` + +### U2. props를 state에 복사하지 마라 + +prop을 useState에 넣으면 부모 변경 무시됨. 직접 사용하거나 `initialX`로 명명. + +```ts +// ❌ const [color, setColor] = useState(messageColor); +// ✅ const color = messageColor; +// ✅ function Message({ initialColor }: ...) { const [color, setColor] = useState(initialColor); } +``` + +### U3. 렌더에 영향 없는 값은 useRef + +interval ID, 이전값, 내부 플래그 → useState 대신 useRef. `ref.current`는 렌더 중 읽기/쓰기 금지. + +```ts +// ❌ const [intervalId, setIntervalId] = useState(null); +// ✅ const intervalRef = useRef(null); +``` + +### U4. 복잡한 관련 state는 useReducer + +3개+ state가 함께 변하거나 업데이트 로직이 흩어지면 useReducer로 통합. 순수 함수 → 테스트 용이. + +```ts +// ❌ 핸들러마다 setTasks(...) 흩어짐 +// ✅ const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); +``` + +### U5. 불가능한 상태를 discriminated union으로 제거 + +N개 boolean → 2^N 조합. 단일 status union으로 불가능한 상태 타입 레벨 차단. + +```ts +// ❌ const [isSending, setIsSending] = useState(false); +// const [isSent, setIsSent] = useState(false); +// ✅ type Status = 'typing' | 'sending' | 'sent'; +// const [status, setStatus] = useState('typing'); +``` + +### U6. 객체 복사 대신 ID 저장 + +리스트에서 선택된 항목을 state에 복사 → 원본 수정 시 stale. ID만 저장 + 렌더 시 파생. + +```ts +// ❌ const [selectedItem, setSelectedItem] = useState(items[0]); +// ✅ const [selectedId, setSelectedId] = useState(items[0].id); +// const selectedItem = items.find(i => i.id === selectedId); +``` + +### U7. 관련 state는 하나의 객체로 그룹화 + +항상 함께 변하는 state → 하나의 setState로 원자적 업데이트. + +```ts +// ❌ const [x, setX] = useState(0); const [y, setY] = useState(0); +// ✅ const [position, setPosition] = useState({ x: 0, y: 0 }); +``` + +--- + +## Effect Usage (7개) + +### U8. useEffect는 외부 시스템 동기화 전용 + +네트워크, DOM API, 브라우저 API 동기화에만 사용. 이벤트 핸들링, 데이터 변환에는 쓰지 마라. + +```ts +// ❌ useEffect(() => { if (product.isInCart) showNotification('Added!'); }, [product]); +// ✅ function handleBuy() { addToCart(product); showNotification('Added!'); } +``` + +### U9. useEffect 체인 금지 + +하나의 effect가 setState → 다음 effect 트리거 → 순차 리렌더 + 추적 불가. 이벤트 핸들러나 reducer로 통합. + +### U10. state 리셋은 key prop으로 + +`key={id}`로 재마운트. useEffect 리셋 → stale 값 한 프레임 노출. + +```ts +// ❌ useEffect(() => { setComment(''); }, [userId]); +// ✅ +``` + +### U11. effect 안에서만 쓰는 객체/함수는 effect 내부에 선언 + +컴포넌트 본문에 선언 → 매 렌더 새 참조 → effect 매번 재실행. + +```ts +// ❌ const options = { serverUrl, roomId }; +// useEffect(() => { connect(options); }, [options]); +// ✅ useEffect(() => { +// const options = { serverUrl, roomId }; +// connect(options); +// }, [roomId]); +``` + +### U12. 외부 스토어 구독은 useSyncExternalStore + +브라우저 API, 서드파티 스토어 구독 → useState+useEffect 대신 useSyncExternalStore. concurrent rendering tearing 방지 + SSR 서버 스냅샷 지원. + +### U13. 부모 알림은 이벤트 핸들러에서 + +자식이 부모에게 state 변경 알릴 때 useEffect가 아닌 같은 이벤트 핸들러에서 콜백 호출. 연쇄 리렌더 방지. + +```ts +// ❌ useEffect(() => { onChange(isOn); }, [isOn]); +// ✅ function handleClick() { setIsOn(!isOn); onChange(!isOn); } +``` + +### U14. 비동기 effect는 반드시 cleanup + +fetch/timer/subscription → cleanup 없으면 race condition. 빠른 prop 변경 시 이전 응답이 이후 응답을 덮어씀. + +```ts +useEffect(function fetchResults() { + let ignore = false; + fetchAPI(query).then(data => { if (!ignore) setResults(data); }); + return () => { ignore = true; }; +}, [query]); +``` + +--- + +## Memoization (2개) + +### U15. useMemo는 1ms 이상 측정된 연산에만 + +`console.time`으로 측정해서 1ms 미만이면 useMemo 오버헤드가 더 큼. + +### U16. useCallback은 memo() 래핑된 자식에 전달할 때만 + +memo() 없는 자식에 stable reference → 리렌더 방지 효과 없음. + +--- + +## Hook Design (1개) + +### U17. 커스텀 훅은 재사용 가능한 상태 로직 추출용 + +lifecycle wrapper(`useMount`, `useEffectOnce`) 금지. 구체적 동기화 목적 훅(`useWindowSize`, `useOnlineStatus`)만. +추출 기준: 동일 state+effect 패턴이 2개+ 컴포넌트에서 반복되는지? diff --git a/packages/plugin/.claude-plugin/plugin.json b/packages/plugin/.claude-plugin/plugin.json new file mode 100644 index 00000000..2d19549a --- /dev/null +++ b/packages/plugin/.claude-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "react-hook-philosophy", + "version": "0.1.0", + "description": "React hook design philosophy for code review and writing. Covers SSR safety, return values, state design, effect patterns, TypeScript, and performance.", + "author": { "name": "kimyouknow" }, + "homepage": "https://react-simplikit.slash.page", + "repository": "https://github.com/kimyouknow/react-simplikit", + "license": "MIT", + "keywords": ["react", "hooks", "philosophy", "code-review", "ssr", "typescript"] +} diff --git a/packages/plugin/README.md b/packages/plugin/README.md new file mode 100644 index 00000000..a0a79443 --- /dev/null +++ b/packages/plugin/README.md @@ -0,0 +1,48 @@ +# react-hook-philosophy + +React hook design philosophy plugin for Claude Code. Two skills for reviewing and writing hooks with principled patterns. + +## Install + +```bash +claude plugin install --source git-subdir \ + --url https://github.com/kimyouknow/react-simplikit.git \ + --path packages/plugin +``` + +## Skills + +### /react-hook-review + +Review hooks against 31 design principles. Structured feedback with severity levels. + +- 14 coding principles (C1-C14): return values, SSR safety, TypeScript, cleanup, performance +- 17 usage patterns (U1-U17): state design, effect usage, memoization, hook design +- Output: Great Work / Required Changes / Suggestions / Next Steps + +### /react-hook-writing + +Write hooks following design philosophy. Themed guide with code examples. + +- State management patterns (derive, useRef vs useState, useReducer) +- Effect patterns (when to use, cleanup, external store subscription) +- TypeScript, performance, JSDoc templates +- Reference implementations in `patterns.md` + +## Principles Overview + +| Category | Count | Examples | +|----------|-------|---------| +| Coding (C1-C14) | 14 | Always return objects, SSR-safe init, no `any`, cleanup | +| State Design (U1-U7) | 7 | Derive don't sync, useRef for non-rendered, discriminated unions | +| Effect Usage (U8-U14) | 7 | Effects for sync only, no chains, key reset, async cleanup | +| Memoization (U15-U16) | 2 | useMemo >= 1ms, useCallback + memo() only | +| Hook Design (U17) | 1 | Extract reusable logic, not lifecycle wrappers | + +## Philosophy + +Every rule includes a "Why" explanation. Opinionated items are transparently marked with trade-offs. + +## License + +MIT diff --git a/packages/plugin/skills/react-hook-review/SKILL.md b/packages/plugin/skills/react-hook-review/SKILL.md new file mode 100644 index 00000000..20800a29 --- /dev/null +++ b/packages/plugin/skills/react-hook-review/SKILL.md @@ -0,0 +1,101 @@ +--- +description: Review React hooks against design philosophy. Checks return values, SSR safety, state design, effect usage, TypeScript patterns, and performance. +--- + +# React Hook Review + +Review hooks against coding principles and usage patterns. Report findings by severity. + +## Coding Principles Checklist + +### Required (13 items) + +1. **Return values (C1)** — Always return objects, even for single values. `{ value }` not bare primitives. + Why: Named fields, order-independent, extensible without breaking changes. + +2. **SSR-safe init (C2)** — `useState(FIXED)` + `useEffect(sync)`. No browser API in initializer. + Why: Server has no `window` — crashes or hydration mismatch. + +3. **Cleanup (C3)** — Every useEffect with side effects returns cleanup (listeners, timers, AbortController). + Why: Memory leaks. StrictMode double-mount exposes missing cleanup immediately. + +4. **No `any` (C4)** — Use generics ``. Justified `eslint-disable` with comment is acceptable. + Why: `any` propagates and defeats the type system. + +5. **Named exports (C5)** — No default exports. Tree-shaking + unambiguous imports. + +6. **Strict booleans (C6)** — `== null` for nullish, `!== undefined` for distinction. No implicit `if (value)`. + Why: `0`, `""`, `false` are falsy — silent bugs. + +7. **Object parameters (C7)** — Hook params as object props, not positional args. + Why: Order-independent, self-documenting, extensible. + +8. **Guard clauses (C8)** — Early return over nested if-else. Flat success path. + +9. **JSDoc 4-tag (C9)** — @description + @param + @returns + @example on every public API. + Why: AI doc generation quality + IDE tooltips. + +10. **Performance (C10)** — Throttle (16ms) for >30 events/sec, deduplicate unchanged, startTransition for non-urgent. + Only applies to high-frequency event hooks. + +11. **Function keyword (C11)** — `function` for declarations, arrows for inline callbacks only. + +12. **Zero deps (C12)** — No runtime dependencies. peerDependencies only. + +13. **Dependency isolation (C13)** — Inject external dependencies as params, don't import directly in hooks. + Why: Testability + replaceability. + +### Recommended (1 item) + +14. **Named useEffect (C14)** — `useEffect(function handleX() {...})` not arrows. + Why: "handleResize" vs "anonymous" in error stacks. Trade-off: more verbose. + +## Usage Patterns Checklist + +### State Design + +- **Derive, don't sync (U1)** — Compute from props/state during render. No `useEffect` for derived values. +- **Don't mirror props (U2)** — Use prop directly or name it `initialX`. +- **useRef for non-rendered (U3)** — Interval IDs, flags, previous values. +- **useReducer for complex (U4)** — 3+ related states changing together. +- **Discriminated unions (U5)** — Replace boolean combos with status union type. +- **IDs not objects (U6)** — Store selected ID, derive object from list. +- **Group related state (U7)** — Always-together values in one object. + +### Effect Usage + +- **Effects for sync only (U8)** — External systems. Not event handling or data transforms. +- **No effect chains (U9)** — Consolidate cascading setState into handlers/reducers. +- **Key reset (U10)** — `key={id}` to remount, not useEffect to clear state. +- **Deps inside effect (U11)** — Objects/functions used only in effect go inside it. +- **useSyncExternalStore (U12)** — For browser API / external store subscriptions. +- **Parent notify in handler (U13)** — Call parent callback in same event handler, not effect. +- **Async cleanup (U14)** — `ignore` flag or AbortController for every async effect. + +### Memoization + +- **useMemo >= 1ms (U15)** — Measure with console.time. Skip if < 1ms. +- **useCallback + memo() (U16)** — Only when child is wrapped in memo(). Otherwise pointless. + +### Hook Design + +- **Extract logic, not lifecycle (U17)** — No `useMount`. Purpose-specific hooks only. + +## Output Format + +### Great Work +- [What was done well] + +### Required Changes +1. **[C#/U#]** Issue description + - Current: `code` + - Suggested: `code` + - Why: [reason] + +### Suggestions +- [Non-blocking improvements] + +### Next Steps +1. Fix required changes +2. Run test suite +3. Commit diff --git a/packages/plugin/skills/react-hook-writing/SKILL.md b/packages/plugin/skills/react-hook-writing/SKILL.md new file mode 100644 index 00000000..31bcd0c5 --- /dev/null +++ b/packages/plugin/skills/react-hook-writing/SKILL.md @@ -0,0 +1,122 @@ +--- +description: Write React hooks following design philosophy. Covers naming, return values, SSR safety, state design, effect patterns, TypeScript, and performance. +--- + +# React Hook Writing Guide + +Design principles for writing React hooks. Each section covers What + Why. + +## 1. API Design + +**Return values (C1):** Always return objects. Even single values use `{ value }`. +Why: Named fields, order-independent, extensible without breaking changes. + +```ts +function useDebounce({ value, delay }: { value: T; delay: number }): { value: T } +function useToggle({ initial }: { initial?: boolean }): { value: boolean; toggle: () => void } +``` + +**Parameters (C7):** Object props, not positional. Order-independent + self-documenting. + +**Named exports (C5):** No default exports. + +## 2. SSR Safety + +**Fixed initial + useEffect sync (C2).** Never call browser APIs in useState initializer. + +```ts +const [width, setWidth] = useState(0); +useEffect(function syncWidth() { + setWidth(window.innerWidth); +}, []); +``` + +For client-only apps: conditional initializer `useState(() => { if (typeof window === 'undefined') return 0; ... })` is acceptable. + +## 3. State Design + +**Derive, don't sync (U1):** Compute from existing state during render. No useEffect for derived values. + +```ts +// Compute during render +const fullName = firstName + ' ' + lastName; +``` + +**useRef for non-rendered values (U3):** Interval IDs, flags, previous values. + +**useReducer for complex state (U4):** 3+ related states changing together. + +**Discriminated unions (U5):** Replace boolean combos with `type Status = 'idle' | 'loading' | 'done'`. + +**Don't mirror props (U2):** Use directly, or name `initialX`. + +**IDs not objects (U6), group related state (U7).** + +## 4. Effect Patterns + +**Effects for sync only (U8).** External systems (network, DOM, browser APIs). Not for event handling or data transforms. + +**No effect chains (U9).** Consolidate cascading setState into event handlers or reducers. + +**Key reset (U10):** `key={id}` to remount cleanly, not useEffect to clear state. + +**Deps inside effect (U11):** Objects/functions used only in effect — define inside. + +**Parent notify in handler (U13):** Call parent callback in same event handler, not effect. + +## 5. Cleanup (C3) + +Every side effect needs cleanup. Three patterns: + +```ts +// Event listeners +return () => window.removeEventListener('resize', handler); + +// Async (AbortController) +const controller = new AbortController(); +return () => controller.abort(); + +// Timers +return () => clearInterval(id); +``` + +Async effects need ignore flags or AbortController to prevent race conditions (U14). + +## 6. Performance (C10) + +Apply only to >30 events/sec (scroll, resize, keyboard): +- **Throttle** at 16ms (60fps) +- **Deduplicate**: skip setState when value unchanged +- **startTransition**: expensive non-urgent computations + +**useMemo (U15):** Only for measured >= 1ms computations. +**useCallback (U16):** Only when passing to memo()-wrapped children. + +## 7. TypeScript + +- **Generics `` (C4):** No `any`. Justified eslint-disable with comment is acceptable. +- **`as const`** for tuple returns (if ever needed). +- **Strict booleans (C6):** `== null` for nullish, `!== undefined` for distinction. +- **Function keyword (C11):** For declarations. Arrows for inline callbacks. + +## 8. Documentation (C9) + +```ts +/** + * @description [One-line summary] + * @param {{ value: T; delay: number }} params - Hook parameters + * @returns {{ value: T }} Debounced value + * @example + * const { value } = useDebounce({ value: query, delay: 300 }); + */ +``` + +## 9. Dependencies + +- **Zero runtime deps (C12):** peerDependencies only. +- **Inject externals (C13):** Pass fetcher/client as param, don't import directly. +- **Extract hooks for reuse (U17):** Same state+effect in 2+ components? Extract. No lifecycle wrappers. + +## Reference + +See [patterns.md](references/patterns.md) for 3 complete hook implementations. diff --git a/packages/plugin/skills/react-hook-writing/references/patterns.md b/packages/plugin/skills/react-hook-writing/references/patterns.md new file mode 100644 index 00000000..0e7cc32d --- /dev/null +++ b/packages/plugin/skills/react-hook-writing/references/patterns.md @@ -0,0 +1,234 @@ +# Hook Implementation Patterns + +Three complete hooks demonstrating the design principles in practice. + +--- + +## 1. useToggle (Simple) + +Demonstrates: C1 (object return), C5 (named export), C7 (object params), C9 (JSDoc), C14 (named useEffect) + +```ts +import { useState, useCallback } from 'react'; + +/** + * @description Manages a boolean toggle state. + * @param {{ initial?: boolean }} params - Hook parameters + * @returns {{ value: boolean; toggle: () => void; setTrue: () => void; setFalse: () => void }} + * @example + * const { value: isOpen, toggle } = useToggle({ initial: false }); + * + */ +export function useToggle({ initial = false }: { initial?: boolean } = {}) { + const [value, setValue] = useState(initial); + + const toggle = useCallback(function toggle() { + setValue(prev => !prev); + }, []); + + const setTrue = useCallback(function setTrue() { + setValue(true); + }, []); + + const setFalse = useCallback(function setFalse() { + setValue(false); + }, []); + + return { value, toggle, setTrue, setFalse }; +} +``` + +### Anti-pattern comparison + +```ts +// Bad: tuple return (C1 violation) +export function useToggle(initial = false): [boolean, () => void] { + // Adding setTrue/setFalse later = breaking change for all consumers +} + +// Bad: default export (C5 violation) +export default function useToggle() { ... } + +// Bad: positional params (C7 violation) +export function useToggle(initial: boolean, onChange?: (v: boolean) => void) { ... } +``` + +--- + +## 2. useDebounce (Intermediate) + +Demonstrates: C1 (object return), C3 (cleanup), C4 (generic), C7 (object params), C9 (JSDoc), C11 (function keyword) + +```ts +import { useState, useEffect, useRef } from 'react'; + +/** + * @description Delays updating a value until after a specified period of inactivity. + * @param {{ value: T; delay: number }} params - The value to debounce and delay in ms + * @returns {{ value: T }} The debounced value + * @example + * const { value: debouncedQuery } = useDebounce({ value: searchQuery, delay: 300 }); + * useEffect(function fetchResults() { + * fetch(`/api/search?q=${debouncedQuery}`); + * }, [debouncedQuery]); + */ +export function useDebounce({ value, delay }: { value: T; delay: number }): { value: T } { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(function scheduleUpdate() { + const timer = setTimeout(function applyUpdate() { + setDebouncedValue(value); + }, delay); + + return function cancelPendingUpdate() { + clearTimeout(timer); + }; + }, [value, delay]); + + return { value: debouncedValue }; +} +``` + +### Anti-pattern comparison + +```ts +// Bad: no cleanup (C3 violation) — timer leak on rapid value changes +useEffect(() => { + setTimeout(() => setDebouncedValue(value), delay); + // Missing: return () => clearTimeout(timer) +}, [value, delay]); + +// Bad: any type (C4 violation) +function useDebounce(value: any, delay: any): any { ... } +``` + +--- + +## 3. useMediaQuery (Complex — SSR-Safe) + +Demonstrates: C1 (object return), C2 (SSR-safe), C3 (cleanup), C10 (performance), C14 (named effect) +Also: U3 (useRef for non-rendered), U8 (effect for external sync) + +```ts +import { useState, useEffect, useRef } from 'react'; + +/** + * @description Tracks whether a CSS media query matches. SSR-safe with fixed initial value. + * @param {{ query: string }} params - The media query string + * @returns {{ matches: boolean }} Whether the media query currently matches + * @example + * const { matches: isMobile } = useMediaQuery({ query: '(max-width: 768px)' }); + * return isMobile ? : ; + */ +export function useMediaQuery({ query }: { query: string }): { matches: boolean } { + const [matches, setMatches] = useState(false); // C2: Fixed initial value (SSR-safe) + const prevMatchesRef = useRef(false); // U3: Non-rendered value + + useEffect(function syncMediaQuery() { + if (typeof window === 'undefined') { + return; + } + + const mediaQueryList = window.matchMedia(query); + + function handleChange() { + const nextMatches = mediaQueryList.matches; + + // C10: Deduplicate — skip if unchanged + if (prevMatchesRef.current === nextMatches) { + return; + } + prevMatchesRef.current = nextMatches; + setMatches(nextMatches); + } + + // Initial sync + handleChange(); + + // Subscribe + mediaQueryList.addEventListener('change', handleChange); + + return function cleanupMediaQuery() { + mediaQueryList.removeEventListener('change', handleChange); + }; + }, [query]); + + return { matches }; +} +``` + +### Anti-pattern comparison + +```ts +// Bad: SSR crash (C2 violation) +const [matches] = useState(window.matchMedia(query).matches); + +// Bad: no cleanup (C3 violation) — listener leak +useEffect(() => { + const mql = window.matchMedia(query); + mql.addEventListener('change', handler); + // Missing: return () => mql.removeEventListener(...) +}, [query]); + +// Bad: no dedup — unnecessary re-renders +function handleChange() { + setMatches(mediaQueryList.matches); // fires even when value unchanged +} +``` + +--- + +## SSR-Safe Hook Template + +Generic template for hooks that access browser APIs: + +```ts +export function useExample({ param }: { param: ParamType }): { value: ReturnType } { + const [value, setValue] = useState(FIXED_INITIAL); // C2: SSR-safe + const prevRef = useRef(FIXED_INITIAL); // U3: non-rendered + + useEffect(function syncBrowserValue() { + if (typeof window === 'undefined') { + return; + } + + // Initial sync + const current = getBrowserValue(param); + prevRef.current = current; + setValue(current); + + // Subscribe to changes + function handleChange() { + const next = getBrowserValue(param); + if (prevRef.current === next) { return; } // C10: dedup + prevRef.current = next; + setValue(next); + } + + window.addEventListener('event', handleChange); + + return function cleanup() { + window.removeEventListener('event', handleChange); + }; + }, [param]); + + return { value }; +} +``` + +--- + +## Anti-Pattern Collection + +| Anti-Pattern | Principle Violated | Fix | +|---|---|---| +| `useState(window.innerWidth)` | C2 (SSR) | `useState(0)` + useEffect sync | +| Missing cleanup on addEventListener | C3 (Cleanup) | Return removeEventListener | +| `function useData(url: any): any` | C4 (No any) | Use generic `` | +| `export default useHook` | C5 (Named exports) | `export function useHook` | +| `if (count)` where count can be 0 | C6 (Strict booleans) | `if (count !== undefined)` | +| `useEffect(() => { setFullName(...) }, [first, last])` | U1 (Derive) | `const fullName = first + last` | +| `const [color] = useState(colorProp)` | U2 (Mirror props) | `const color = colorProp` | +| `const [id, setId] = useState(null)` for non-rendered | U3 (useRef) | `useRef(null)` | +| chained useEffects setting state | U9 (No chains) | Consolidate in handler | +| `useMemo(() => items.filter(...), [items])` on 20 items | U15 (Measure first) | Plain computation | From a118d945104f6ce67debcd0de348b2baecb24cc6 Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Mon, 6 Apr 2026 12:00:50 +0900 Subject: [PATCH 02/14] fix: address PR review feedback for plugin - Move C8 (guard clauses) and C11 (function keyword) from Required to Recommended - Add U12 (useSyncExternalStore) to writing guide - Fix anti-pattern table: count check uses == null instead of !== undefined - Add MIT LICENSE file --- packages/plugin/LICENSE | 21 ++++++++++++++++++ .../plugin/skills/react-hook-review/SKILL.md | 22 ++++++++++--------- .../plugin/skills/react-hook-writing/SKILL.md | 2 ++ .../react-hook-writing/references/patterns.md | 2 +- 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 packages/plugin/LICENSE diff --git a/packages/plugin/LICENSE b/packages/plugin/LICENSE new file mode 100644 index 00000000..5737c5a3 --- /dev/null +++ b/packages/plugin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 kimyouknow + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/plugin/skills/react-hook-review/SKILL.md b/packages/plugin/skills/react-hook-review/SKILL.md index 20800a29..61dc6e36 100644 --- a/packages/plugin/skills/react-hook-review/SKILL.md +++ b/packages/plugin/skills/react-hook-review/SKILL.md @@ -8,7 +8,7 @@ Review hooks against coding principles and usage patterns. Report findings by se ## Coding Principles Checklist -### Required (13 items) +### Required (11 items) 1. **Return values (C1)** — Always return objects, even for single values. `{ value }` not bare primitives. Why: Named fields, order-independent, extensible without breaking changes. @@ -30,22 +30,24 @@ Review hooks against coding principles and usage patterns. Report findings by se 7. **Object parameters (C7)** — Hook params as object props, not positional args. Why: Order-independent, self-documenting, extensible. -8. **Guard clauses (C8)** — Early return over nested if-else. Flat success path. - -9. **JSDoc 4-tag (C9)** — @description + @param + @returns + @example on every public API. +8. **JSDoc 4-tag (C9)** — @description + @param + @returns + @example on every public API. Why: AI doc generation quality + IDE tooltips. -10. **Performance (C10)** — Throttle (16ms) for >30 events/sec, deduplicate unchanged, startTransition for non-urgent. +9. **Performance (C10)** — Throttle (16ms) for >30 events/sec, deduplicate unchanged, startTransition for non-urgent. Only applies to high-frequency event hooks. -11. **Function keyword (C11)** — `function` for declarations, arrows for inline callbacks only. - -12. **Zero deps (C12)** — No runtime dependencies. peerDependencies only. +10. **Zero deps (C12)** — No runtime dependencies. peerDependencies only. -13. **Dependency isolation (C13)** — Inject external dependencies as params, don't import directly in hooks. +11. **Dependency isolation (C13)** — Inject external dependencies as parameters rather than importing them directly inside hooks. Why: Testability + replaceability. -### Recommended (1 item) +### Recommended (3 items) + +12. **Guard clauses (C8)** — Early return over nested if-else. Flat success path. + Trade-off: Stylistic preference with no functional impact. + +13. **Function keyword (C11)** — `function` for declarations, arrows for inline callbacks only. + Trade-off: Consistent style, but arrow declarations are valid JS. 14. **Named useEffect (C14)** — `useEffect(function handleX() {...})` not arrows. Why: "handleResize" vs "anonymous" in error stacks. Trade-off: more verbose. diff --git a/packages/plugin/skills/react-hook-writing/SKILL.md b/packages/plugin/skills/react-hook-writing/SKILL.md index 31bcd0c5..6a361b1d 100644 --- a/packages/plugin/skills/react-hook-writing/SKILL.md +++ b/packages/plugin/skills/react-hook-writing/SKILL.md @@ -64,6 +64,8 @@ const fullName = firstName + ' ' + lastName; **Parent notify in handler (U13):** Call parent callback in same event handler, not effect. +**useSyncExternalStore (U12):** For browser API or third-party store subscriptions, prefer `useSyncExternalStore` over `useState` + `useEffect`. Prevents tearing in concurrent rendering and supports SSR server snapshots. + ## 5. Cleanup (C3) Every side effect needs cleanup. Three patterns: diff --git a/packages/plugin/skills/react-hook-writing/references/patterns.md b/packages/plugin/skills/react-hook-writing/references/patterns.md index 0e7cc32d..5d2f9abb 100644 --- a/packages/plugin/skills/react-hook-writing/references/patterns.md +++ b/packages/plugin/skills/react-hook-writing/references/patterns.md @@ -226,7 +226,7 @@ export function useExample({ param }: { param: ParamType }): { value: ReturnType | Missing cleanup on addEventListener | C3 (Cleanup) | Return removeEventListener | | `function useData(url: any): any` | C4 (No any) | Use generic `` | | `export default useHook` | C5 (Named exports) | `export function useHook` | -| `if (count)` where count can be 0 | C6 (Strict booleans) | `if (count !== undefined)` | +| `if (count)` where count can be 0 | C6 (Strict booleans) | `if (count != null)` | | `useEffect(() => { setFullName(...) }, [first, last])` | U1 (Derive) | `const fullName = first + last` | | `const [color] = useState(colorProp)` | U2 (Mirror props) | `const color = colorProp` | | `const [id, setId] = useState(null)` for non-rendered | U3 (useRef) | `useRef(null)` | From 6510de65eaaddd1839452fa6ffbdc97bce6a1c7b Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 7 Apr 2026 22:07:37 +0900 Subject: [PATCH 03/14] docs: translate design docs to English, add Korean versions to docs/ko/ - Convert hook-design-principles.md and react-hook-usage-patterns.md to English - Add react.dev source URLs and quotes to all U1-U17 patterns - Move Korean originals to docs/ko/ for reference --- docs/hook-design-principles.md | 287 +++++++++++------------ docs/ko/hook-design-principles.md | 329 +++++++++++++++++++++++++++ docs/ko/react-hook-usage-patterns.md | 164 +++++++++++++ docs/react-hook-usage-patterns.md | 140 ++++++++---- 4 files changed, 733 insertions(+), 187 deletions(-) create mode 100644 docs/ko/hook-design-principles.md create mode 100644 docs/ko/react-hook-usage-patterns.md diff --git a/docs/hook-design-principles.md b/docs/hook-design-principles.md index e557b5e8..a6658ec3 100644 --- a/docs/hook-design-principles.md +++ b/docs/hook-design-principles.md @@ -1,58 +1,59 @@ # React Hook Design Principles -> 최종 업데이트: 2026-04-03 -> 상태: Draft (논의 후 확정) +> Last Updated: 2026-04-07 +> Status: Draft (pending discussion) +> Korean version: [ko/hook-design-principles.md](./ko/hook-design-principles.md) --- -## 1. 요구사항 +## 1. Requirements -### 배경 +### Background -react-simplikit을 운영하며 축적한 훅 설계 철학을 **하나의 공통 원칙**으로 정의한다. 이 원칙은 두 가지 용도로 사용된다: +Hook design philosophy accumulated from operating react-simplikit is defined as **a single set of shared principles**. These principles serve two purposes: -1. **코드 리뷰** — `react-hook-review` 스킬이 이 원칙 기반으로 피드백 -2. **코드 작성** — `react-hook-writing` 스킬이 이 원칙 기반으로 가이드 +1. **Code review** — `react-hook-review` skill provides feedback based on these principles +2. **Code writing** — `react-hook-writing` skill provides guidance based on these principles -### 원칙의 두 가지 방향 +### Two Directions of Principles -| 방향 | 출처 | 범위 | -|------|------|------| -| **훅 코딩 원칙** (Section 2) | CLAUDE.md, AGENTS.md, 내부 스킬 | 반환값, TypeScript, 성능, 문서화 등 코딩 스타일 | -| **훅 사용 패턴** (Section 3) | React 공식 문서 (react.dev) | state 설계, effect 사용법, 메모이제이션, 커스텀 훅 설계 | +| Direction | Source | Scope | +|-----------|--------|-------| +| **Coding Principles** (Section 2) | CLAUDE.md, AGENTS.md, internal skills | Return values, TypeScript, performance, documentation | +| **Usage Patterns** (Section 3) | React official docs (react.dev) | State design, effect usage, memoization, custom hook design | -### 핵심 요구사항 +### Core Requirements -| # | 요구사항 | 상세 | -|---|---------|------| -| R1 | 리뷰/생성 공통 원칙 | 두 스킬이 동일한 원칙 참조 | -| R2 | Why 중심 | 규칙(What)만 나열하지 않고 철학(Why)을 narrative로 설명 | -| R3 | Opinionated 투명성 | 🟢 Best Practice vs 🟡 Opinionated 명시 | -| R4 | 프로젝트 무관 | react-simplikit 경로/명령어/유틸 없이 범용 원칙만 | -| R5 | Cross-tool | Claude Code 플러그인 + Codex(AGENTS.md) + Cursor(.cursorrules) | +| # | Requirement | Detail | +|---|------------|--------| +| R1 | Shared principles for review/writing | Both skills reference the same principles | +| R2 | Why-first | Not just rules (What), but philosophy (Why) with narrative explanation | +| R3 | Opinionated transparency | Clearly mark 🟢 Best Practice vs 🟡 Opinionated | +| R4 | Project-agnostic | No react-simplikit paths/commands/utils — universal principles only | +| R5 | Cross-tool | Claude Code plugin + Codex (AGENTS.md) + Cursor (.cursorrules) | -### 결정 필요 사항 +### Open Questions -| # | 질문 | 선택지 | -|---|------|--------| -| Q1 | C14(Named useEffect)를 포함할지? | A) "Recommended"로 포함 B) 제외 | -| Q2 | C2(SSR-Safe)를 비-SSR 프로젝트에도 권장할지? | A) 항상 B) SSR 사용 시만 | -| Q3 | C9(JSDoc)의 @example을 필수로 할지? | A) 4-tag 전부 필수 B) @example은 권장 | -| Q4 | 추가할 원칙이 있는지? | — | -| Q5 | 원칙 먼저 확정할지, 바로 플러그인 구조로 갈지? | A) 원칙 먼저 B) 바로 플러그인 | -| Q6 | 플러그인 배포 채널 | A) git-subdir B) npm C) 미정 | +| # | Question | Options | +|---|---------|---------| +| Q1 | Include C14 (Named useEffect)? | A) Include as "Recommended" B) Exclude | +| Q2 | Recommend C2 (SSR-Safe) for non-SSR projects? | A) Always B) SSR projects only | +| Q3 | Require @example in C9 (JSDoc)? | A) All 4 tags required B) @example is recommended | +| Q4 | Any additional principles? | — | +| Q5 | Finalize principles first, or go straight to plugin structure? | A) Principles first B) Plugin directly | +| Q6 | Plugin distribution channel | A) git-subdir B) npm C) TBD | --- -## 2. 훅 코딩 원칙 (Direction 1) +## 2. Hook Coding Principles (Direction 1) -CLAUDE.md + AGENTS.md + 내부 스킬에서 추출한 **코딩 스타일** 원칙. +Coding style principles extracted from CLAUDE.md + AGENTS.md + internal skills. -### 🟢 Best Practice (13개) +### 🟢 Best Practice (13) -#### C1. 항상 객체 반환 +#### C1. Always Return Objects -반환값이 1개여도 `{ value }` 형태. 객체는 순서 무관, 이름으로 의미 전달, 확장 시 breaking change 없음. +Return objects even for single values — `{ value }` form. Objects are order-independent, self-documenting via named fields, and extensible without breaking changes. ```ts function useDebounce(value: T, delay: number): { value: T } @@ -60,44 +61,44 @@ function useToggle(init: boolean): { value: boolean; toggle: () => void } function usePagination(): { page: number; next: () => void; prev: () => void } ``` -#### C2. SSR-Safe 초기화 +#### C2. SSR-Safe Initialization -`useState(FIXED_VALUE)` + `useEffect(sync)`. 브라우저 API 초기화 금지. 서버에 `window` 없음 → 크래시 또는 hydration mismatch. +`useState(FIXED_VALUE)` + `useEffect(sync)`. Never initialize state with browser APIs. Server has no `window` — crashes or hydration mismatch. ```ts -// ✅ SSR 안전 +// ✅ SSR safe const [width, setWidth] = useState(0); useEffect(function syncWidth() { setWidth(window.innerWidth); }, []); -// ❌ SSR 크래시 +// ❌ SSR crash const [width, setWidth] = useState(window.innerWidth); -// ⚠️ 클라이언트 전용 앱에서만 허용 +// ⚠️ Acceptable in client-only apps const [width, setWidth] = useState(() => { if (typeof window === 'undefined') return 0; return window.innerWidth; }); ``` -#### C3. useEffect Cleanup 필수 +#### C3. useEffect Cleanup Required -모든 부수효과에 cleanup 반환. 메모리 누수 방지. StrictMode 이중 마운트가 즉시 노출. +Return cleanup from every side effect. Prevents memory leaks. StrictMode double-mount exposes missing cleanup immediately. ```ts -// 이벤트 리스너 +// Event listeners useEffect(function subscribe() { window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); }, []); -// AbortController (비동기) +// AbortController (async) useEffect(function fetchData() { const controller = new AbortController(); fetch(url, { signal: controller.signal }).then(/* ... */); return () => controller.abort(); }, [url]); -// 타이머 +// Timers useEffect(function tick() { const id = setInterval(callback, 1000); return () => clearInterval(id); @@ -106,48 +107,48 @@ useEffect(function tick() { #### C4. No `any` Types -제네릭 `` 사용. any 전파 → 타입 시스템 무력화. 정당한 사유(generic callback 등) 시 per-line eslint-disable + 코멘트 허용. +Use generics ``. `any` propagates and defeats the type system. Justified `eslint-disable` with comment is acceptable for generic callback types. ```ts // ✅ Generic function useDebounce(value: T, delay: number): T -// ✅ 정당한 예외 (코멘트 필수) +// ✅ Justified exception (comment required) // eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic callback type AnyFunction = (...args: any[]) => unknown; ``` #### C5. Named Exports Only -tree-shaking 보장 + import 명확성. `export default` 금지. +Guarantees tree-shaking + unambiguous imports. No `export default`. #### C6. Strict Boolean & Nullish Checks -`if (value)` 금지 → 0, "", false falsy 버그 방지. `== null`로 null+undefined 동시 체크. +No implicit `if (value)` — prevents silent bugs with `0`, `""`, `false`. Use `== null` for nullish checks (both null and undefined). ```ts if (ref == null) { return; } // ✅ null + undefined -const controlled = valueProp !== undefined; // ✅ 구분 필요할 때 -if (count) { ... } // ❌ count=0 통과 못함 +const controlled = valueProp !== undefined; // ✅ when distinction needed +if (count) { ... } // ❌ fails when count = 0 ``` -#### C7. Parameter는 객체로 받기 +#### C7. Object Parameters -훅의 인자를 개별 파라미터 대신 객체(props)로. 순서 무관 + 이름으로 의미 전달 + 확장 시 breaking change 없음. +Hook params as object props, not positional args. Order-independent, self-documenting, extensible without breaking changes. ```ts -// ✅ 객체 +// ✅ Object params function useDebounce({ value, delay, leading }: { value: T; delay: number; leading?: boolean; }): { value: T } -// ❌ 위치 기반 +// ❌ Positional params function useDebounce(value: T, delay: number, leading?: boolean): { value: T } ``` #### C8. Guard Clauses (Early Return) -nested if-else 대신 early return. 실패 조건 먼저 걸러내고 성공 로직은 플랫하게. +Early return over nested if-else. Filter failure conditions first, keep success logic flat. ```ts // ✅ @@ -164,14 +165,14 @@ function process(value: string | null) { #### C9. JSDoc 4-Tag -모든 public API에 `@description` + `@param` + `@returns` + `@example`. AI 문서 생성 + IDE 툴팁. +All public APIs must have `@description` + `@param` + `@returns` + `@example`. Enables AI doc generation + IDE tooltips. ```ts /** - * @description 값의 변경을 지연시킨다. - * @param value - 디바운스할 값 - * @param delay - 지연 시간 (ms) - * @returns 디바운스된 값 + * @description Delays value updates until after a specified period of inactivity. + * @param value - The value to debounce + * @param delay - Delay in milliseconds + * @returns The debounced value * @example * const debouncedQuery = useDebounce(query, 300); */ @@ -179,151 +180,151 @@ function process(value: string | null) { #### C10. Performance Patterns -고빈도(30+/sec) 이벤트에만 적용. 일반 훅에는 불필요. +Apply only to high-frequency events (30+/sec). Not needed for general hooks. -| 기법 | 적용 시점 | -|------|-----------| +| Technique | When to Apply | +|-----------|--------------| | Throttle (16ms) | scroll, resize, pointer, keyboard | -| Deduplicate | 값 미변경 시 setState skip | -| startTransition | 비긴급 파생 계산 (React 18+) | +| Deduplicate | Skip setState when value unchanged | +| startTransition | Non-urgent derived computations (React 18+) | #### C11. Function Keyword for Declarations -함수 선언은 `function` 키워드. 화살표는 인라인 콜백(map, filter)에만. +Use `function` keyword for declarations. Arrows only for inline callbacks (map, filter). ```ts -function toggle(state: boolean) { return !state; } // ✅ 선언 -items.filter(item => item != null); // ✅ 인라인 -const toggle = (state: boolean) => !state; // ❌ 선언에 화살표 +function toggle(state: boolean) { return !state; } // ✅ declaration +items.filter(item => item != null); // ✅ inline +const toggle = (state: boolean) => !state; // ❌ arrow for declaration ``` #### C12. Zero Runtime Dependencies -프로덕션 코드에 외부 런타임 의존성 금지. `peerDependencies`만 허용. 번들 사이즈 최소화 + 의존성 충돌 방지. +No external runtime dependencies in production code. Only `peerDependencies` allowed. Minimizes bundle size + prevents dependency conflicts. -#### C13. 외부 의존성 직접 참조 지양 +#### C13. Avoid Direct External Dependencies -훅 내부에서 외부 모듈을 직접 호출하지 않고 인자로 주입. 테스트 용이성 + 교체 가능성. +Inject external dependencies as parameters rather than importing directly inside hooks. Improves testability + replaceability. ```ts -// ✅ 의존성 주입 +// ✅ Dependency injection function useFetch(fetcher: (url: string) => Promise, url: string) { ... } -// ❌ 외부 모듈 직접 +// ❌ Direct import function useFetch(url: string) { const res = await axios.get(url); ... } ``` -### 🟡 Opinionated (1개) +### 🟡 Opinionated (1) #### C14. Named useEffect Functions -`useEffect(function handleResize() {...})`. 에러 스택에서 "handleResize" vs "anonymous". Trade-off: 화살표보다 장황. cleanup 이름은 "Recommended" (필수 아님). +`useEffect(function handleResize() {...})`. Shows "handleResize" instead of "anonymous" in error stacks. Trade-off: more verbose than arrows. Named cleanup is "Recommended" (not required). -### 제외 (프로젝트별 결정) +### Excluded (Project-Specific Decisions) -| 항목 | 이유 | -|------|------| -| Import extensions (.js/.ts) | 빌드 도구 의존적 | -| 100% test coverage | 프로젝트 정책 | -| 파일 구조/커밋 컨벤션 | 훅 설계 철학 아님 | +| Item | Reason | +|------|--------| +| Import extensions (.js/.ts) | Build-tool dependent | +| 100% test coverage | Project policy | +| File structure / commit conventions | Not hook design philosophy | --- -## 3. 훅 사용 패턴 (Direction 2) +## 3. Hook Usage Patterns (Direction 2) -> 별도 문서: [react-hook-usage-patterns.md](./react-hook-usage-patterns.md) +> Separate document: [react-hook-usage-patterns.md](./react-hook-usage-patterns.md) -React 공식 문서(react.dev) 기반 17개 패턴 (U1-U17): +17 patterns based on React official docs (react.dev), with source URLs and quotes (U1-U17): -| 카테고리 | 개수 | 핵심 | -|----------|------|------| -| State Design | U1-U7 | 파생값 계산, props 복사 금지, useRef, useReducer, union type | -| Effect Usage | U8-U14 | effect는 외부 동기화 전용, 체인 금지, key 리셋, 비동기 cleanup | -| Memoization | U15-U16 | useMemo 1ms+, useCallback + memo() 조합만 | -| Hook Design | U17 | lifecycle wrapper 금지, 구체적 목적 훅만 | +| Category | Count | Key Patterns | +|----------|-------|-------------| +| State Design | U1-U7 | Derive don't sync, don't mirror props, useRef, useReducer, discriminated unions | +| Effect Usage | U8-U14 | Effects for sync only, no chains, key reset, async cleanup | +| Memoization | U15-U16 | useMemo >= 1ms, useCallback + memo() only | +| Hook Design | U17 | No lifecycle wrappers, extract reusable stateful logic only | --- -## 4. 플러그인 아키텍처 +## 4. Plugin Architecture -### 파생 흐름 +### Derivation Flow ``` -이 문서 (principles, 원칙 정의) - ↓ 압축 -react-hook-review/SKILL.md (체크리스트) -react-hook-writing/SKILL.md (가이드) - ↓ 추가 압축 -AGENTS.md Part 1 (Codex용) - ↓ 참조 -.cursorrules (Cursor용) +This document (principles definition) + ↓ compress +react-hook-review/SKILL.md (checklist) +react-hook-writing/SKILL.md (guide) + ↓ further compress +AGENTS.md Part 1 (for Codex) + ↓ reference +.cursorrules (for Cursor) ``` -### 디렉토리 구조 +### Directory Structure ``` packages/plugin/ (planned) ├── .claude-plugin/plugin.json ├── .codex-plugin/plugin.json -├── principles/ ← 공통 원칙 Single Source +├── principles/ ← Shared principles single source ├── skills/ -│ ├── react-hook-review/SKILL.md ← C1-C14 + U1-U17 체크리스트 +│ ├── react-hook-review/SKILL.md ← C1-C14 + U1-U17 checklist │ └── react-hook-writing/ -│ ├── SKILL.md ← 테마별 가이드 -│ └── references/patterns.md ← 구현 예시 3개 +│ ├── SKILL.md ← Themed guide +│ └── references/patterns.md ← 3 hook implementations └── README.md ``` -### Cross-Tool 지원 +### Cross-Tool Support -| 도구 | 파일 | 현재 | 변경 | -|------|------|------|------| -| Claude Code (내부) | `.claude/skills/` | ✅ 10개 | 유지 | -| Claude Code (플러그인) | `packages/plugin/` | ❌ | Phase 1-5로 생성 | -| Codex | `AGENTS.md` | ✅ 162줄 | Part 1(Universal) + Part 2(Project) 분리 | -| Cursor | `.cursorrules` | ✅ 28줄 | AGENTS.md 참조 유지 | +| Tool | File | Current | Planned | +|------|------|---------|---------| +| Claude Code (internal) | `.claude/skills/` | ✅ 10 skills | Keep | +| Claude Code (plugin) | `packages/plugin/` | ❌ | Create via Phase 1-5 | +| Codex | `AGENTS.md` | ✅ 162 lines | Split into Part 1 (Universal) + Part 2 (Project) | +| Cursor | `.cursorrules` | ✅ 28 lines | Keep AGENTS.md reference | -### 추출 규칙 +### Extraction Rules -| 추출됨 (철학) | 남겨짐 (구현) | -|--------------|-------------| -| "항상 객체 반환" | `packages/core/src/hooks/` 경로 | -| "Named useEffect improves stack traces" | `yarn test`, `yarn fix` 명령 | -| "SSR-safe: fixed initial + useEffect sync" | `renderHookSSR.serverOnly()` 유틸 | -| "4 JSDoc tags for AI doc generation" | `100%` coverage 기준 | +| Extracted (Philosophy) | Left Behind (Implementation) | +|----------------------|---------------------------| +| "Always return objects" | `packages/core/src/hooks/` paths | +| "Named useEffect improves stack traces" | `yarn test`, `yarn fix` commands | +| "SSR-safe: fixed initial + useEffect sync" | `renderHookSSR.serverOnly()` utility | +| "4 JSDoc tags for AI doc generation" | `100%` coverage threshold | -### 일반화 변환 +### Generalization Transforms -| Before (프로젝트 전용) | After (범용) | +| Before (Project-Specific) | After (Universal) | |---|---| | `renderHookSSR.serverOnly()` | Vitest + `delete global.window` | | `yarn test` / `yarn fix` | "Run your test suite" | -| `packages/core/` 경로 | "your source directory" | -| `react-simplikit` 언급 | 제거 | +| `packages/core/` paths | "your source directory" | +| `react-simplikit` references | Removed | --- -## 5. 실행 로드맵 +## 5. Execution Roadmap -| Phase | 내용 | 산출물 | -|-------|------|--------| -| 1 | 디렉토리 + plugin.json + README | `packages/plugin/` 구조 | -| 2 | react-hook-review SKILL.md | C1-C14 + U1-U17 체크리스트 | -| 3 | react-hook-writing SKILL.md + patterns.md | 테마별 가이드 + 3개 훅 예시 | -| 4 | 일반화 검증 (grep) | 프로젝트 참조 0건 | -| 5 | 플러그인 validate + 로컬 테스트 | 동작 확인 | +| Phase | Content | Output | +|-------|---------|--------| +| 1 | Directory + plugin.json + README | `packages/plugin/` structure | +| 2 | react-hook-review SKILL.md | C1-C14 + U1-U17 checklist | +| 3 | react-hook-writing SKILL.md + patterns.md | Themed guide + 3 hook examples | +| 4 | Generalization validation (grep) | 0 project references | +| 5 | Plugin validate + local test | Working confirmation | -### 검증 기준 +### Validation Criteria -| 항목 | 통과 기준 | -|------|---------| -| 플러그인 구조 | `claude plugin validate .` 에러 0 | -| 범용성 | 다른 React 프로젝트에서 프로젝트 참조 0건 | -| 철학 깊이 | 각 규칙의 Why가 narrative | -| Opinionated 투명성 | 🟡 패턴에 trade-off 존재 | +| Item | Pass Criteria | +|------|-------------| +| Plugin structure | `claude plugin validate .` — 0 errors | +| Universality | 0 project-specific references in another React project | +| Philosophy depth | Every rule has narrative "Why" | +| Opinionated transparency | 🟡 items have trade-offs stated | -### 향후 확장 +### Future Expansion -- Codex/Gemini 대응 (AGENTS.md Part 1 활용) -- Component 설계 철학 추가 -- Marketplace 전환 (Plugin 3개+ 시) +- Codex/Gemini support (via AGENTS.md Part 1) +- Component design philosophy +- Marketplace migration (when 3+ plugins) diff --git a/docs/ko/hook-design-principles.md b/docs/ko/hook-design-principles.md new file mode 100644 index 00000000..e557b5e8 --- /dev/null +++ b/docs/ko/hook-design-principles.md @@ -0,0 +1,329 @@ +# React Hook Design Principles + +> 최종 업데이트: 2026-04-03 +> 상태: Draft (논의 후 확정) + +--- + +## 1. 요구사항 + +### 배경 + +react-simplikit을 운영하며 축적한 훅 설계 철학을 **하나의 공통 원칙**으로 정의한다. 이 원칙은 두 가지 용도로 사용된다: + +1. **코드 리뷰** — `react-hook-review` 스킬이 이 원칙 기반으로 피드백 +2. **코드 작성** — `react-hook-writing` 스킬이 이 원칙 기반으로 가이드 + +### 원칙의 두 가지 방향 + +| 방향 | 출처 | 범위 | +|------|------|------| +| **훅 코딩 원칙** (Section 2) | CLAUDE.md, AGENTS.md, 내부 스킬 | 반환값, TypeScript, 성능, 문서화 등 코딩 스타일 | +| **훅 사용 패턴** (Section 3) | React 공식 문서 (react.dev) | state 설계, effect 사용법, 메모이제이션, 커스텀 훅 설계 | + +### 핵심 요구사항 + +| # | 요구사항 | 상세 | +|---|---------|------| +| R1 | 리뷰/생성 공통 원칙 | 두 스킬이 동일한 원칙 참조 | +| R2 | Why 중심 | 규칙(What)만 나열하지 않고 철학(Why)을 narrative로 설명 | +| R3 | Opinionated 투명성 | 🟢 Best Practice vs 🟡 Opinionated 명시 | +| R4 | 프로젝트 무관 | react-simplikit 경로/명령어/유틸 없이 범용 원칙만 | +| R5 | Cross-tool | Claude Code 플러그인 + Codex(AGENTS.md) + Cursor(.cursorrules) | + +### 결정 필요 사항 + +| # | 질문 | 선택지 | +|---|------|--------| +| Q1 | C14(Named useEffect)를 포함할지? | A) "Recommended"로 포함 B) 제외 | +| Q2 | C2(SSR-Safe)를 비-SSR 프로젝트에도 권장할지? | A) 항상 B) SSR 사용 시만 | +| Q3 | C9(JSDoc)의 @example을 필수로 할지? | A) 4-tag 전부 필수 B) @example은 권장 | +| Q4 | 추가할 원칙이 있는지? | — | +| Q5 | 원칙 먼저 확정할지, 바로 플러그인 구조로 갈지? | A) 원칙 먼저 B) 바로 플러그인 | +| Q6 | 플러그인 배포 채널 | A) git-subdir B) npm C) 미정 | + +--- + +## 2. 훅 코딩 원칙 (Direction 1) + +CLAUDE.md + AGENTS.md + 내부 스킬에서 추출한 **코딩 스타일** 원칙. + +### 🟢 Best Practice (13개) + +#### C1. 항상 객체 반환 + +반환값이 1개여도 `{ value }` 형태. 객체는 순서 무관, 이름으로 의미 전달, 확장 시 breaking change 없음. + +```ts +function useDebounce(value: T, delay: number): { value: T } +function useToggle(init: boolean): { value: boolean; toggle: () => void } +function usePagination(): { page: number; next: () => void; prev: () => void } +``` + +#### C2. SSR-Safe 초기화 + +`useState(FIXED_VALUE)` + `useEffect(sync)`. 브라우저 API 초기화 금지. 서버에 `window` 없음 → 크래시 또는 hydration mismatch. + +```ts +// ✅ SSR 안전 +const [width, setWidth] = useState(0); +useEffect(function syncWidth() { setWidth(window.innerWidth); }, []); + +// ❌ SSR 크래시 +const [width, setWidth] = useState(window.innerWidth); + +// ⚠️ 클라이언트 전용 앱에서만 허용 +const [width, setWidth] = useState(() => { + if (typeof window === 'undefined') return 0; + return window.innerWidth; +}); +``` + +#### C3. useEffect Cleanup 필수 + +모든 부수효과에 cleanup 반환. 메모리 누수 방지. StrictMode 이중 마운트가 즉시 노출. + +```ts +// 이벤트 리스너 +useEffect(function subscribe() { + window.addEventListener('resize', handler); + return () => window.removeEventListener('resize', handler); +}, []); + +// AbortController (비동기) +useEffect(function fetchData() { + const controller = new AbortController(); + fetch(url, { signal: controller.signal }).then(/* ... */); + return () => controller.abort(); +}, [url]); + +// 타이머 +useEffect(function tick() { + const id = setInterval(callback, 1000); + return () => clearInterval(id); +}, []); +``` + +#### C4. No `any` Types + +제네릭 `` 사용. any 전파 → 타입 시스템 무력화. 정당한 사유(generic callback 등) 시 per-line eslint-disable + 코멘트 허용. + +```ts +// ✅ Generic +function useDebounce(value: T, delay: number): T + +// ✅ 정당한 예외 (코멘트 필수) +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic callback +type AnyFunction = (...args: any[]) => unknown; +``` + +#### C5. Named Exports Only + +tree-shaking 보장 + import 명확성. `export default` 금지. + +#### C6. Strict Boolean & Nullish Checks + +`if (value)` 금지 → 0, "", false falsy 버그 방지. `== null`로 null+undefined 동시 체크. + +```ts +if (ref == null) { return; } // ✅ null + undefined +const controlled = valueProp !== undefined; // ✅ 구분 필요할 때 +if (count) { ... } // ❌ count=0 통과 못함 +``` + +#### C7. Parameter는 객체로 받기 + +훅의 인자를 개별 파라미터 대신 객체(props)로. 순서 무관 + 이름으로 의미 전달 + 확장 시 breaking change 없음. + +```ts +// ✅ 객체 +function useDebounce({ value, delay, leading }: { + value: T; delay: number; leading?: boolean; +}): { value: T } + +// ❌ 위치 기반 +function useDebounce(value: T, delay: number, leading?: boolean): { value: T } +``` + +#### C8. Guard Clauses (Early Return) + +nested if-else 대신 early return. 실패 조건 먼저 걸러내고 성공 로직은 플랫하게. + +```ts +// ✅ +function process(value: string | null) { + if (value == null) { return DEFAULT; } + return transform(value); +} + +// ❌ +function process(value: string | null) { + if (value != null) { return transform(value); } else { return DEFAULT; } +} +``` + +#### C9. JSDoc 4-Tag + +모든 public API에 `@description` + `@param` + `@returns` + `@example`. AI 문서 생성 + IDE 툴팁. + +```ts +/** + * @description 값의 변경을 지연시킨다. + * @param value - 디바운스할 값 + * @param delay - 지연 시간 (ms) + * @returns 디바운스된 값 + * @example + * const debouncedQuery = useDebounce(query, 300); + */ +``` + +#### C10. Performance Patterns + +고빈도(30+/sec) 이벤트에만 적용. 일반 훅에는 불필요. + +| 기법 | 적용 시점 | +|------|-----------| +| Throttle (16ms) | scroll, resize, pointer, keyboard | +| Deduplicate | 값 미변경 시 setState skip | +| startTransition | 비긴급 파생 계산 (React 18+) | + +#### C11. Function Keyword for Declarations + +함수 선언은 `function` 키워드. 화살표는 인라인 콜백(map, filter)에만. + +```ts +function toggle(state: boolean) { return !state; } // ✅ 선언 +items.filter(item => item != null); // ✅ 인라인 +const toggle = (state: boolean) => !state; // ❌ 선언에 화살표 +``` + +#### C12. Zero Runtime Dependencies + +프로덕션 코드에 외부 런타임 의존성 금지. `peerDependencies`만 허용. 번들 사이즈 최소화 + 의존성 충돌 방지. + +#### C13. 외부 의존성 직접 참조 지양 + +훅 내부에서 외부 모듈을 직접 호출하지 않고 인자로 주입. 테스트 용이성 + 교체 가능성. + +```ts +// ✅ 의존성 주입 +function useFetch(fetcher: (url: string) => Promise, url: string) { ... } + +// ❌ 외부 모듈 직접 +function useFetch(url: string) { const res = await axios.get(url); ... } +``` + +### 🟡 Opinionated (1개) + +#### C14. Named useEffect Functions + +`useEffect(function handleResize() {...})`. 에러 스택에서 "handleResize" vs "anonymous". Trade-off: 화살표보다 장황. cleanup 이름은 "Recommended" (필수 아님). + +### 제외 (프로젝트별 결정) + +| 항목 | 이유 | +|------|------| +| Import extensions (.js/.ts) | 빌드 도구 의존적 | +| 100% test coverage | 프로젝트 정책 | +| 파일 구조/커밋 컨벤션 | 훅 설계 철학 아님 | + +--- + +## 3. 훅 사용 패턴 (Direction 2) + +> 별도 문서: [react-hook-usage-patterns.md](./react-hook-usage-patterns.md) + +React 공식 문서(react.dev) 기반 17개 패턴 (U1-U17): + +| 카테고리 | 개수 | 핵심 | +|----------|------|------| +| State Design | U1-U7 | 파생값 계산, props 복사 금지, useRef, useReducer, union type | +| Effect Usage | U8-U14 | effect는 외부 동기화 전용, 체인 금지, key 리셋, 비동기 cleanup | +| Memoization | U15-U16 | useMemo 1ms+, useCallback + memo() 조합만 | +| Hook Design | U17 | lifecycle wrapper 금지, 구체적 목적 훅만 | + +--- + +## 4. 플러그인 아키텍처 + +### 파생 흐름 + +``` +이 문서 (principles, 원칙 정의) + ↓ 압축 +react-hook-review/SKILL.md (체크리스트) +react-hook-writing/SKILL.md (가이드) + ↓ 추가 압축 +AGENTS.md Part 1 (Codex용) + ↓ 참조 +.cursorrules (Cursor용) +``` + +### 디렉토리 구조 + +``` +packages/plugin/ (planned) +├── .claude-plugin/plugin.json +├── .codex-plugin/plugin.json +├── principles/ ← 공통 원칙 Single Source +├── skills/ +│ ├── react-hook-review/SKILL.md ← C1-C14 + U1-U17 체크리스트 +│ └── react-hook-writing/ +│ ├── SKILL.md ← 테마별 가이드 +│ └── references/patterns.md ← 구현 예시 3개 +└── README.md +``` + +### Cross-Tool 지원 + +| 도구 | 파일 | 현재 | 변경 | +|------|------|------|------| +| Claude Code (내부) | `.claude/skills/` | ✅ 10개 | 유지 | +| Claude Code (플러그인) | `packages/plugin/` | ❌ | Phase 1-5로 생성 | +| Codex | `AGENTS.md` | ✅ 162줄 | Part 1(Universal) + Part 2(Project) 분리 | +| Cursor | `.cursorrules` | ✅ 28줄 | AGENTS.md 참조 유지 | + +### 추출 규칙 + +| 추출됨 (철학) | 남겨짐 (구현) | +|--------------|-------------| +| "항상 객체 반환" | `packages/core/src/hooks/` 경로 | +| "Named useEffect improves stack traces" | `yarn test`, `yarn fix` 명령 | +| "SSR-safe: fixed initial + useEffect sync" | `renderHookSSR.serverOnly()` 유틸 | +| "4 JSDoc tags for AI doc generation" | `100%` coverage 기준 | + +### 일반화 변환 + +| Before (프로젝트 전용) | After (범용) | +|---|---| +| `renderHookSSR.serverOnly()` | Vitest + `delete global.window` | +| `yarn test` / `yarn fix` | "Run your test suite" | +| `packages/core/` 경로 | "your source directory" | +| `react-simplikit` 언급 | 제거 | + +--- + +## 5. 실행 로드맵 + +| Phase | 내용 | 산출물 | +|-------|------|--------| +| 1 | 디렉토리 + plugin.json + README | `packages/plugin/` 구조 | +| 2 | react-hook-review SKILL.md | C1-C14 + U1-U17 체크리스트 | +| 3 | react-hook-writing SKILL.md + patterns.md | 테마별 가이드 + 3개 훅 예시 | +| 4 | 일반화 검증 (grep) | 프로젝트 참조 0건 | +| 5 | 플러그인 validate + 로컬 테스트 | 동작 확인 | + +### 검증 기준 + +| 항목 | 통과 기준 | +|------|---------| +| 플러그인 구조 | `claude plugin validate .` 에러 0 | +| 범용성 | 다른 React 프로젝트에서 프로젝트 참조 0건 | +| 철학 깊이 | 각 규칙의 Why가 narrative | +| Opinionated 투명성 | 🟡 패턴에 trade-off 존재 | + +### 향후 확장 + +- Codex/Gemini 대응 (AGENTS.md Part 1 활용) +- Component 설계 철학 추가 +- Marketplace 전환 (Plugin 3개+ 시) diff --git a/docs/ko/react-hook-usage-patterns.md b/docs/ko/react-hook-usage-patterns.md new file mode 100644 index 00000000..67eae610 --- /dev/null +++ b/docs/ko/react-hook-usage-patterns.md @@ -0,0 +1,164 @@ +# React Hook Usage Patterns + +> 최종 업데이트: 2026-04-03 +> 출처: React 공식 문서 (react.dev) +> 관련: [Hook Design Principles](./hook-design-principles.md) + +코딩 스타일이 아닌 **hooks를 올바르게 사용하는 패턴**. 17개 원칙. + +--- + +## State Design (7개) + +### U1. 파생 가능한 값은 state에 넣지 마라 + +기존 props/state에서 계산 가능한 값은 렌더 중에 계산. useEffect 동기화 → 1렌더 지연 + 불필요한 추가 렌더. + +```ts +// ❌ const [fullName, setFullName] = useState(''); +// useEffect(() => { setFullName(first + ' ' + last); }, [first, last]); +// ✅ const fullName = first + ' ' + last; +``` + +### U2. props를 state에 복사하지 마라 + +prop을 useState에 넣으면 부모 변경 무시됨. 직접 사용하거나 `initialX`로 명명. + +```ts +// ❌ const [color, setColor] = useState(messageColor); +// ✅ const color = messageColor; +// ✅ function Message({ initialColor }: ...) { const [color, setColor] = useState(initialColor); } +``` + +### U3. 렌더에 영향 없는 값은 useRef + +interval ID, 이전값, 내부 플래그 → useState 대신 useRef. `ref.current`는 렌더 중 읽기/쓰기 금지. + +```ts +// ❌ const [intervalId, setIntervalId] = useState(null); +// ✅ const intervalRef = useRef(null); +``` + +### U4. 복잡한 관련 state는 useReducer + +3개+ state가 함께 변하거나 업데이트 로직이 흩어지면 useReducer로 통합. 순수 함수 → 테스트 용이. + +```ts +// ❌ 핸들러마다 setTasks(...) 흩어짐 +// ✅ const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); +``` + +### U5. 불가능한 상태를 discriminated union으로 제거 + +N개 boolean → 2^N 조합. 단일 status union으로 불가능한 상태 타입 레벨 차단. + +```ts +// ❌ const [isSending, setIsSending] = useState(false); +// const [isSent, setIsSent] = useState(false); +// ✅ type Status = 'typing' | 'sending' | 'sent'; +// const [status, setStatus] = useState('typing'); +``` + +### U6. 객체 복사 대신 ID 저장 + +리스트에서 선택된 항목을 state에 복사 → 원본 수정 시 stale. ID만 저장 + 렌더 시 파생. + +```ts +// ❌ const [selectedItem, setSelectedItem] = useState(items[0]); +// ✅ const [selectedId, setSelectedId] = useState(items[0].id); +// const selectedItem = items.find(i => i.id === selectedId); +``` + +### U7. 관련 state는 하나의 객체로 그룹화 + +항상 함께 변하는 state → 하나의 setState로 원자적 업데이트. + +```ts +// ❌ const [x, setX] = useState(0); const [y, setY] = useState(0); +// ✅ const [position, setPosition] = useState({ x: 0, y: 0 }); +``` + +--- + +## Effect Usage (7개) + +### U8. useEffect는 외부 시스템 동기화 전용 + +네트워크, DOM API, 브라우저 API 동기화에만 사용. 이벤트 핸들링, 데이터 변환에는 쓰지 마라. + +```ts +// ❌ useEffect(() => { if (product.isInCart) showNotification('Added!'); }, [product]); +// ✅ function handleBuy() { addToCart(product); showNotification('Added!'); } +``` + +### U9. useEffect 체인 금지 + +하나의 effect가 setState → 다음 effect 트리거 → 순차 리렌더 + 추적 불가. 이벤트 핸들러나 reducer로 통합. + +### U10. state 리셋은 key prop으로 + +`key={id}`로 재마운트. useEffect 리셋 → stale 값 한 프레임 노출. + +```ts +// ❌ useEffect(() => { setComment(''); }, [userId]); +// ✅ +``` + +### U11. effect 안에서만 쓰는 객체/함수는 effect 내부에 선언 + +컴포넌트 본문에 선언 → 매 렌더 새 참조 → effect 매번 재실행. + +```ts +// ❌ const options = { serverUrl, roomId }; +// useEffect(() => { connect(options); }, [options]); +// ✅ useEffect(() => { +// const options = { serverUrl, roomId }; +// connect(options); +// }, [roomId]); +``` + +### U12. 외부 스토어 구독은 useSyncExternalStore + +브라우저 API, 서드파티 스토어 구독 → useState+useEffect 대신 useSyncExternalStore. concurrent rendering tearing 방지 + SSR 서버 스냅샷 지원. + +### U13. 부모 알림은 이벤트 핸들러에서 + +자식이 부모에게 state 변경 알릴 때 useEffect가 아닌 같은 이벤트 핸들러에서 콜백 호출. 연쇄 리렌더 방지. + +```ts +// ❌ useEffect(() => { onChange(isOn); }, [isOn]); +// ✅ function handleClick() { setIsOn(!isOn); onChange(!isOn); } +``` + +### U14. 비동기 effect는 반드시 cleanup + +fetch/timer/subscription → cleanup 없으면 race condition. 빠른 prop 변경 시 이전 응답이 이후 응답을 덮어씀. + +```ts +useEffect(function fetchResults() { + let ignore = false; + fetchAPI(query).then(data => { if (!ignore) setResults(data); }); + return () => { ignore = true; }; +}, [query]); +``` + +--- + +## Memoization (2개) + +### U15. useMemo는 1ms 이상 측정된 연산에만 + +`console.time`으로 측정해서 1ms 미만이면 useMemo 오버헤드가 더 큼. + +### U16. useCallback은 memo() 래핑된 자식에 전달할 때만 + +memo() 없는 자식에 stable reference → 리렌더 방지 효과 없음. + +--- + +## Hook Design (1개) + +### U17. 커스텀 훅은 재사용 가능한 상태 로직 추출용 + +lifecycle wrapper(`useMount`, `useEffectOnce`) 금지. 구체적 동기화 목적 훅(`useWindowSize`, `useOnlineStatus`)만. +추출 기준: 동일 state+effect 패턴이 2개+ 컴포넌트에서 반복되는지? diff --git a/docs/react-hook-usage-patterns.md b/docs/react-hook-usage-patterns.md index 67eae610..2611df60 100644 --- a/docs/react-hook-usage-patterns.md +++ b/docs/react-hook-usage-patterns.md @@ -1,18 +1,22 @@ # React Hook Usage Patterns -> 최종 업데이트: 2026-04-03 -> 출처: React 공식 문서 (react.dev) -> 관련: [Hook Design Principles](./hook-design-principles.md) +> Last Updated: 2026-04-07 +> Source: React official documentation (react.dev) +> Related: [Hook Design Principles](./hook-design-principles.md) +> Korean version: [ko/react-hook-usage-patterns.md](./ko/react-hook-usage-patterns.md) -코딩 스타일이 아닌 **hooks를 올바르게 사용하는 패턴**. 17개 원칙. +Patterns for **correctly using hooks** — not coding style, but React-specific best practices. 17 principles. --- -## State Design (7개) +## State Design (7) -### U1. 파생 가능한 값은 state에 넣지 마라 +### U1. Derive Instead of Syncing with State -기존 props/state에서 계산 가능한 값은 렌더 중에 계산. useEffect 동기화 → 1렌더 지연 + 불필요한 추가 렌더. +If a value can be computed from existing props or state, calculate it during render. Syncing with useEffect causes a 1-render delay + unnecessary extra render. + +> 📖 [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) +> *"If something can be calculated from the existing props or state, don't put it in state. Instead, calculate it during rendering."* ```ts // ❌ const [fullName, setFullName] = useState(''); @@ -20,9 +24,12 @@ // ✅ const fullName = first + ' ' + last; ``` -### U2. props를 state에 복사하지 마라 +### U2. Don't Mirror Props in State + +Copying a prop into useState means parent changes are silently ignored. Use the prop directly, or name it `initialX` if intentional. -prop을 useState에 넣으면 부모 변경 무시됨. 직접 사용하거나 `initialX`로 명명. +> 📖 [Choosing the State Structure — Avoid redundant state](https://react.dev/learn/choosing-the-state-structure#avoid-redundant-state) +> *"If you can calculate some information from the component's props or its existing state variables during rendering, you should not put that information into that component's state."* ```ts // ❌ const [color, setColor] = useState(messageColor); @@ -30,27 +37,36 @@ prop을 useState에 넣으면 부모 변경 무시됨. 직접 사용하거나 `i // ✅ function Message({ initialColor }: ...) { const [color, setColor] = useState(initialColor); } ``` -### U3. 렌더에 영향 없는 값은 useRef +### U3. Use useRef for Non-Rendered Values + +Interval IDs, previous values, internal flags — use useRef instead of useState. Avoids unnecessary re-renders. Never read/write `ref.current` during rendering. -interval ID, 이전값, 내부 플래그 → useState 대신 useRef. `ref.current`는 렌더 중 읽기/쓰기 금지. +> 📖 [Referencing Values with Refs](https://react.dev/learn/referencing-values-with-refs) +> *"When you want a component to 'remember' some information, but you don't want that information to trigger new renders, you can use a ref."* ```ts // ❌ const [intervalId, setIntervalId] = useState(null); // ✅ const intervalRef = useRef(null); ``` -### U4. 복잡한 관련 state는 useReducer +### U4. Use useReducer for Complex Related State -3개+ state가 함께 변하거나 업데이트 로직이 흩어지면 useReducer로 통합. 순수 함수 → 테스트 용이. +When 3+ state values change together or update logic is scattered across handlers, consolidate into useReducer. Pure function — easy to test. + +> 📖 [Extracting State Logic into a Reducer](https://react.dev/learn/extracting-state-logic-into-a-reducer) +> *"To reduce complexity and keep all your logic in one easy-to-access place, you can move that state logic into a single function outside your component, called a 'reducer'."* ```ts -// ❌ 핸들러마다 setTasks(...) 흩어짐 +// ❌ Scattered setTasks(...) across handlers // ✅ const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); ``` -### U5. 불가능한 상태를 discriminated union으로 제거 +### U5. Eliminate Impossible States with Discriminated Unions + +N booleans → 2^N combinations with invalid states. A single status union type prevents impossible states at the type level. -N개 boolean → 2^N 조합. 단일 status union으로 불가능한 상태 타입 레벨 차단. +> 📖 [Choosing the State Structure — Avoid contradictions in state](https://react.dev/learn/choosing-the-state-structure#avoid-contradictions-in-state) +> *"Since isSending and isSent should never be true at the same time, it is better to replace them with one status state variable."* ```ts // ❌ const [isSending, setIsSending] = useState(false); @@ -59,9 +75,12 @@ N개 boolean → 2^N 조합. 단일 status union으로 불가능한 상태 타 // const [status, setStatus] = useState('typing'); ``` -### U6. 객체 복사 대신 ID 저장 +### U6. Store IDs Instead of Duplicating Objects -리스트에서 선택된 항목을 state에 복사 → 원본 수정 시 stale. ID만 저장 + 렌더 시 파생. +Copying a selected item from a list into state → stale when source updates. Store the ID and derive during render. + +> 📖 [Choosing the State Structure — Avoid duplication in state](https://react.dev/learn/choosing-the-state-structure#avoid-duplication-in-state) +> *"If you were to duplicate the selected item object, you'd have a problem: if you edit the item, the selected version wouldn't update."* ```ts // ❌ const [selectedItem, setSelectedItem] = useState(items[0]); @@ -69,9 +88,12 @@ N개 boolean → 2^N 조합. 단일 status union으로 불가능한 상태 타 // const selectedItem = items.find(i => i.id === selectedId); ``` -### U7. 관련 state는 하나의 객체로 그룹화 +### U7. Group Related State into a Single Object + +State values that always change together → single setState for atomic updates. -항상 함께 변하는 state → 하나의 setState로 원자적 업데이트. +> 📖 [Choosing the State Structure — Group related state](https://react.dev/learn/choosing-the-state-structure#group-related-state) +> *"If some two state variables always change together, it might be a good idea to unify them into a single state variable."* ```ts // ❌ const [x, setX] = useState(0); const [y, setY] = useState(0); @@ -80,33 +102,45 @@ N개 boolean → 2^N 조합. 단일 status union으로 불가능한 상태 타 --- -## Effect Usage (7개) +## Effect Usage (7) -### U8. useEffect는 외부 시스템 동기화 전용 +### U8. useEffect Is for External System Synchronization Only -네트워크, DOM API, 브라우저 API 동기화에만 사용. 이벤트 핸들링, 데이터 변환에는 쓰지 마라. +Network, DOM APIs, browser APIs — synchronization only. Not for event handling or data transformation. + +> 📖 [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) +> *"Effects are an escape hatch from the React paradigm. They let you 'step outside' of React and synchronize your components with some external system."* ```ts // ❌ useEffect(() => { if (product.isInCart) showNotification('Added!'); }, [product]); // ✅ function handleBuy() { addToCart(product); showNotification('Added!'); } ``` -### U9. useEffect 체인 금지 +### U9. No useEffect Chains + +One effect sets state → triggers next effect → cascading re-renders + untraceable. Consolidate in event handlers or reducers. -하나의 effect가 setState → 다음 effect 트리거 → 순차 리렌더 + 추적 불가. 이벤트 핸들러나 reducer로 통합. +> 📖 [You Might Not Need an Effect — Chains of computations](https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations) +> *"Each setState call triggers a re-render. The component would re-render three times before it has even finished rendering."* -### U10. state 리셋은 key prop으로 +### U10. Reset State with key Prop -`key={id}`로 재마운트. useEffect 리셋 → stale 값 한 프레임 노출. +`key={id}` forces a clean remount. useEffect reset → stale value visible for one frame. + +> 📖 [You Might Not Need an Effect — Resetting all state when a prop changes](https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes) +> *"You can tell React to treat it as a conceptually different component by giving it an explicit key."* ```ts // ❌ useEffect(() => { setComment(''); }, [userId]); // ✅ ``` -### U11. effect 안에서만 쓰는 객체/함수는 effect 내부에 선언 +### U11. Declare Effect-Only Objects/Functions Inside the Effect + +Objects/functions declared in the component body get new references every render → effect re-runs every render. -컴포넌트 본문에 선언 → 매 렌더 새 참조 → effect 매번 재실행. +> 📖 [Removing Effect Dependencies — Move dynamic objects and functions inside your Effect](https://react.dev/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect) +> *"If your Effect depends on an object or a function created during rendering, it might run too often."* ```ts // ❌ const options = { serverUrl, roomId }; @@ -117,22 +151,31 @@ N개 boolean → 2^N 조합. 단일 status union으로 불가능한 상태 타 // }, [roomId]); ``` -### U12. 외부 스토어 구독은 useSyncExternalStore +### U12. Use useSyncExternalStore for External Store Subscriptions + +Browser APIs, third-party stores → use useSyncExternalStore instead of useState + useEffect. Prevents tearing in concurrent rendering + supports SSR server snapshots. + +> 📖 [You Might Not Need an Effect — Subscribing to an external store](https://react.dev/learn/you-might-not-need-an-effect#subscribing-to-an-external-store) +> *"Although it's common to use Effects for this, React has a purpose-built Hook for subscribing to an external store that is preferred instead."* -브라우저 API, 서드파티 스토어 구독 → useState+useEffect 대신 useSyncExternalStore. concurrent rendering tearing 방지 + SSR 서버 스냅샷 지원. +### U13. Notify Parents from Event Handlers -### U13. 부모 알림은 이벤트 핸들러에서 +When a child needs to notify a parent about state changes, call the parent's callback in the same event handler — not in useEffect. Prevents cascading re-renders. -자식이 부모에게 state 변경 알릴 때 useEffect가 아닌 같은 이벤트 핸들러에서 콜백 호출. 연쇄 리렌더 방지. +> 📖 [You Might Not Need an Effect — Notifying parent components about state changes](https://react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes) +> *"You'd want to call onChange during the event handler instead."* ```ts // ❌ useEffect(() => { onChange(isOn); }, [isOn]); // ✅ function handleClick() { setIsOn(!isOn); onChange(!isOn); } ``` -### U14. 비동기 effect는 반드시 cleanup +### U14. Async Effects Must Have Cleanup -fetch/timer/subscription → cleanup 없으면 race condition. 빠른 prop 변경 시 이전 응답이 이후 응답을 덮어씀. +fetch/timer/subscription without cleanup → race condition. Fast prop changes cause older responses to overwrite newer ones. + +> 📖 [Synchronizing with Effects — Fetching data](https://react.dev/learn/synchronizing-with-effects#fetching-data) +> *"The cleanup function should either abort the fetch or ensure its result gets ignored."* ```ts useEffect(function fetchResults() { @@ -144,21 +187,30 @@ useEffect(function fetchResults() { --- -## Memoization (2개) +## Memoization (2) + +### U15. useMemo Only for Measured Expensive Computations -### U15. useMemo는 1ms 이상 측정된 연산에만 +Measure with `console.time`. If under 1ms, useMemo overhead exceeds saved computation. -`console.time`으로 측정해서 1ms 미만이면 useMemo 오버헤드가 더 큼. +> 📖 [useMemo — How to tell if a calculation is expensive](https://react.dev/reference/react/useMemo#how-to-tell-if-a-calculation-is-expensive) +> *"If the overall logged time adds up to a significant amount (say, 1ms or more), it might make sense to memoize that calculation."* -### U16. useCallback은 memo() 래핑된 자식에 전달할 때만 +### U16. useCallback Only When Passing to memo()-Wrapped Children -memo() 없는 자식에 stable reference → 리렌더 방지 효과 없음. +Stable reference to a non-memo() child has zero re-render prevention effect. + +> 📖 [useCallback](https://react.dev/reference/react/useCallback) +> *"You should only rely on useCallback as a performance optimization. If your code doesn't work without it, find the underlying problem first."* --- -## Hook Design (1개) +## Hook Design (1) + +### U17. Extract Custom Hooks for Reusable Stateful Logic -### U17. 커스텀 훅은 재사용 가능한 상태 로직 추출용 +No lifecycle wrappers (`useMount`, `useEffectOnce`). Only purpose-specific hooks (`useWindowSize`, `useOnlineStatus`). +Extraction criterion: Does the same state+effect pattern repeat in 2+ components? -lifecycle wrapper(`useMount`, `useEffectOnce`) 금지. 구체적 동기화 목적 훅(`useWindowSize`, `useOnlineStatus`)만. -추출 기준: 동일 state+effect 패턴이 2개+ 컴포넌트에서 반복되는지? +> 📖 [Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) +> *"Custom Hooks let you share stateful logic, not state itself."* From dff1b323a9fd631cc2dd724dc312fc3750a8e41f Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 7 Apr 2026 22:37:48 +0900 Subject: [PATCH 04/14] docs: remove U4 (useReducer) from hook usage patterns useReducer is an app-level state management pattern, not a hook library design principle. Removed from all docs and plugin review checklist. --- docs/hook-design-principles.md | 2 +- docs/ko/hook-design-principles.md | 2 +- docs/ko/react-hook-usage-patterns.md | 11 +---------- docs/react-hook-usage-patterns.md | 14 +------------- packages/plugin/skills/react-hook-review/SKILL.md | 1 - 5 files changed, 4 insertions(+), 26 deletions(-) diff --git a/docs/hook-design-principles.md b/docs/hook-design-principles.md index a6658ec3..03f4ead1 100644 --- a/docs/hook-design-principles.md +++ b/docs/hook-design-principles.md @@ -238,7 +238,7 @@ function useFetch(url: string) { const res = await axios.get(url); ... } | Category | Count | Key Patterns | |----------|-------|-------------| -| State Design | U1-U7 | Derive don't sync, don't mirror props, useRef, useReducer, discriminated unions | +| State Design | U1-U7 | Derive don't sync, don't mirror props, useRef, discriminated unions, group state | | Effect Usage | U8-U14 | Effects for sync only, no chains, key reset, async cleanup | | Memoization | U15-U16 | useMemo >= 1ms, useCallback + memo() only | | Hook Design | U17 | No lifecycle wrappers, extract reusable stateful logic only | diff --git a/docs/ko/hook-design-principles.md b/docs/ko/hook-design-principles.md index e557b5e8..968a102c 100644 --- a/docs/ko/hook-design-principles.md +++ b/docs/ko/hook-design-principles.md @@ -237,7 +237,7 @@ React 공식 문서(react.dev) 기반 17개 패턴 (U1-U17): | 카테고리 | 개수 | 핵심 | |----------|------|------| -| State Design | U1-U7 | 파생값 계산, props 복사 금지, useRef, useReducer, union type | +| State Design | U1-U7 | 파생값 계산, props 복사 금지, useRef, union type, state 그룹화 | | Effect Usage | U8-U14 | effect는 외부 동기화 전용, 체인 금지, key 리셋, 비동기 cleanup | | Memoization | U15-U16 | useMemo 1ms+, useCallback + memo() 조합만 | | Hook Design | U17 | lifecycle wrapper 금지, 구체적 목적 훅만 | diff --git a/docs/ko/react-hook-usage-patterns.md b/docs/ko/react-hook-usage-patterns.md index 67eae610..e7157bfd 100644 --- a/docs/ko/react-hook-usage-patterns.md +++ b/docs/ko/react-hook-usage-patterns.md @@ -8,7 +8,7 @@ --- -## State Design (7개) +## State Design (6개) ### U1. 파생 가능한 값은 state에 넣지 마라 @@ -39,15 +39,6 @@ interval ID, 이전값, 내부 플래그 → useState 대신 useRef. `ref.curren // ✅ const intervalRef = useRef(null); ``` -### U4. 복잡한 관련 state는 useReducer - -3개+ state가 함께 변하거나 업데이트 로직이 흩어지면 useReducer로 통합. 순수 함수 → 테스트 용이. - -```ts -// ❌ 핸들러마다 setTasks(...) 흩어짐 -// ✅ const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); -``` - ### U5. 불가능한 상태를 discriminated union으로 제거 N개 boolean → 2^N 조합. 단일 status union으로 불가능한 상태 타입 레벨 차단. diff --git a/docs/react-hook-usage-patterns.md b/docs/react-hook-usage-patterns.md index 2611df60..6a012054 100644 --- a/docs/react-hook-usage-patterns.md +++ b/docs/react-hook-usage-patterns.md @@ -9,7 +9,7 @@ Patterns for **correctly using hooks** — not coding style, but React-specific --- -## State Design (7) +## State Design (6) ### U1. Derive Instead of Syncing with State @@ -49,18 +49,6 @@ Interval IDs, previous values, internal flags — use useRef instead of useState // ✅ const intervalRef = useRef(null); ``` -### U4. Use useReducer for Complex Related State - -When 3+ state values change together or update logic is scattered across handlers, consolidate into useReducer. Pure function — easy to test. - -> 📖 [Extracting State Logic into a Reducer](https://react.dev/learn/extracting-state-logic-into-a-reducer) -> *"To reduce complexity and keep all your logic in one easy-to-access place, you can move that state logic into a single function outside your component, called a 'reducer'."* - -```ts -// ❌ Scattered setTasks(...) across handlers -// ✅ const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); -``` - ### U5. Eliminate Impossible States with Discriminated Unions N booleans → 2^N combinations with invalid states. A single status union type prevents impossible states at the type level. diff --git a/packages/plugin/skills/react-hook-review/SKILL.md b/packages/plugin/skills/react-hook-review/SKILL.md index 61dc6e36..edf69257 100644 --- a/packages/plugin/skills/react-hook-review/SKILL.md +++ b/packages/plugin/skills/react-hook-review/SKILL.md @@ -59,7 +59,6 @@ Review hooks against coding principles and usage patterns. Report findings by se - **Derive, don't sync (U1)** — Compute from props/state during render. No `useEffect` for derived values. - **Don't mirror props (U2)** — Use prop directly or name it `initialX`. - **useRef for non-rendered (U3)** — Interval IDs, flags, previous values. -- **useReducer for complex (U4)** — 3+ related states changing together. - **Discriminated unions (U5)** — Replace boolean combos with status union type. - **IDs not objects (U6)** — Store selected ID, derive object from list. - **Group related state (U7)** — Always-together values in one object. From 047a162e10c15cd65c893e92ab30024765958ec7 Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 7 Apr 2026 23:04:22 +0900 Subject: [PATCH 05/14] docs: verify sources and fix inaccurate quotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C1, C7: mark as project conventions with notes (React allows arbitrary returns) - C3: correct "cleanup required" → "cleanup when subscribing" (React says optional) - C2: add react.dev/hydrateRoot source URL - U6, U9, U10, U11, U13, U14, U16: replace paraphrased quotes with actual react.dev text --- docs/hook-design-principles.md | 18 ++++++++++++++---- docs/react-hook-usage-patterns.md | 14 +++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/hook-design-principles.md b/docs/hook-design-principles.md index 03f4ead1..4892b87a 100644 --- a/docs/hook-design-principles.md +++ b/docs/hook-design-principles.md @@ -51,10 +51,13 @@ Coding style principles extracted from CLAUDE.md + AGENTS.md + internal skills. ### 🟢 Best Practice (13) -#### C1. Always Return Objects +#### C1. Always Return Objects 🟡 Return objects even for single values — `{ value }` form. Objects are order-independent, self-documenting via named fields, and extensible without breaking changes. +> Note: This is a **project convention**. React docs say "Hooks may return arbitrary values." React's own `useState` returns a tuple. We chose objects for extensibility. +> 📖 [react.dev — Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) + ```ts function useDebounce(value: T, delay: number): { value: T } function useToggle(init: boolean): { value: boolean; toggle: () => void } @@ -65,6 +68,8 @@ function usePagination(): { page: number; next: () => void; prev: () => void } `useState(FIXED_VALUE)` + `useEffect(sync)`. Never initialize state with browser APIs. Server has no `window` — crashes or hydration mismatch. +> 📖 [react.dev — hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot) + ```ts // ✅ SSR safe const [width, setWidth] = useState(0); @@ -80,9 +85,12 @@ const [width, setWidth] = useState(() => { }); ``` -#### C3. useEffect Cleanup Required +#### C3. useEffect Cleanup When Subscribing -Return cleanup from every side effect. Prevents memory leaks. StrictMode double-mount exposes missing cleanup immediately. +Return cleanup when your effect sets up subscriptions, listeners, timers, or ongoing connections. React docs: cleanup is *optional*, not required for every effect — but mandatory when synchronizing with external systems. + +> 📖 [react.dev — useEffect](https://react.dev/reference/react/useEffect) +> *"Your setup function may also optionally return a cleanup function."* ```ts // Event listeners @@ -132,10 +140,12 @@ const controlled = valueProp !== undefined; // ✅ when distinction needed if (count) { ... } // ❌ fails when count = 0 ``` -#### C7. Object Parameters +#### C7. Object Parameters 🟡 Hook params as object props, not positional args. Order-independent, self-documenting, extensible without breaking changes. +> Note: This is a **project convention**. React's own hooks use positional args (`useState(initialValue)`). We chose objects for extensibility and self-documentation. + ```ts // ✅ Object params function useDebounce({ value, delay, leading }: { diff --git a/docs/react-hook-usage-patterns.md b/docs/react-hook-usage-patterns.md index 6a012054..b45d7aab 100644 --- a/docs/react-hook-usage-patterns.md +++ b/docs/react-hook-usage-patterns.md @@ -68,7 +68,7 @@ N booleans → 2^N combinations with invalid states. A single status union type Copying a selected item from a list into state → stale when source updates. Store the ID and derive during render. > 📖 [Choosing the State Structure — Avoid duplication in state](https://react.dev/learn/choosing-the-state-structure#avoid-duplication-in-state) -> *"If you were to duplicate the selected item object, you'd have a problem: if you edit the item, the selected version wouldn't update."* +> *"The contents of the selectedItem is the same object as one of the items inside the items list. This means that the information about the item itself is duplicated in two places."* ```ts // ❌ const [selectedItem, setSelectedItem] = useState(items[0]); @@ -109,14 +109,14 @@ Network, DOM APIs, browser APIs — synchronization only. Not for event handling One effect sets state → triggers next effect → cascading re-renders + untraceable. Consolidate in event handlers or reducers. > 📖 [You Might Not Need an Effect — Chains of computations](https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations) -> *"Each setState call triggers a re-render. The component would re-render three times before it has even finished rendering."* +> *"The component (and its children) have to re-render between each set call in the chain."* ### U10. Reset State with key Prop `key={id}` forces a clean remount. useEffect reset → stale value visible for one frame. > 📖 [You Might Not Need an Effect — Resetting all state when a prop changes](https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes) -> *"You can tell React to treat it as a conceptually different component by giving it an explicit key."* +> *"Instead, you can tell React that each user's profile is conceptually a different profile by giving it an explicit key."* ```ts // ❌ useEffect(() => { setComment(''); }, [userId]); @@ -128,7 +128,7 @@ One effect sets state → triggers next effect → cascading re-renders + untrac Objects/functions declared in the component body get new references every render → effect re-runs every render. > 📖 [Removing Effect Dependencies — Move dynamic objects and functions inside your Effect](https://react.dev/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect) -> *"If your Effect depends on an object or a function created during rendering, it might run too often."* +> *"Object and function dependencies can make your Effect re-synchronize more often than you need."* ```ts // ❌ const options = { serverUrl, roomId }; @@ -151,7 +151,7 @@ Browser APIs, third-party stores → use useSyncExternalStore instead of useStat When a child needs to notify a parent about state changes, call the parent's callback in the same event handler — not in useEffect. Prevents cascading re-renders. > 📖 [You Might Not Need an Effect — Notifying parent components about state changes](https://react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes) -> *"You'd want to call onChange during the event handler instead."* +> *"Delete the Effect and instead update the state of both components within the same event handler."* ```ts // ❌ useEffect(() => { onChange(isOn); }, [isOn]); @@ -163,7 +163,7 @@ When a child needs to notify a parent about state changes, call the parent's cal fetch/timer/subscription without cleanup → race condition. Fast prop changes cause older responses to overwrite newer ones. > 📖 [Synchronizing with Effects — Fetching data](https://react.dev/learn/synchronizing-with-effects#fetching-data) -> *"The cleanup function should either abort the fetch or ensure its result gets ignored."* +> *"If your Effect fetches something, the cleanup function should either abort the fetch or ignore its result."* ```ts useEffect(function fetchResults() { @@ -189,7 +189,7 @@ Measure with `console.time`. If under 1ms, useMemo overhead exceeds saved comput Stable reference to a non-memo() child has zero re-render prevention effect. > 📖 [useCallback](https://react.dev/reference/react/useCallback) -> *"You should only rely on useCallback as a performance optimization. If your code doesn't work without it, find the underlying problem first."* +> *"You should only rely on useCallback as a performance optimization. If your code doesn't work without it, find the underlying problem and fix it first. Then you may add useCallback back."* --- From d5a7cdf0d1b97d3dd0f1813eaff5db034cdc70f2 Mon Sep 17 00:00:00 2001 From: zztnrudzz13 Date: Wed, 15 Apr 2026 05:54:52 +0900 Subject: [PATCH 06/14] feat: add react design principle skills in claude plugin --- packages/plugin/.claude-plugin/plugin.json | 4 +- packages/plugin/README.md | 12 +++- .../skills/react-design-principles/SKILL.md | 56 +++++++++++++++++++ .../references/principles.md | 44 +++++++++++++++ .../plugin/skills/react-hook-review/SKILL.md | 11 ++++ .../plugin/skills/react-hook-writing/SKILL.md | 8 +++ 6 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 packages/plugin/skills/react-design-principles/SKILL.md create mode 100644 packages/plugin/skills/react-design-principles/references/principles.md diff --git a/packages/plugin/.claude-plugin/plugin.json b/packages/plugin/.claude-plugin/plugin.json index 2d19549a..89a2d741 100644 --- a/packages/plugin/.claude-plugin/plugin.json +++ b/packages/plugin/.claude-plugin/plugin.json @@ -1,10 +1,10 @@ { "name": "react-hook-philosophy", "version": "0.1.0", - "description": "React hook design philosophy for code review and writing. Covers SSR safety, return values, state design, effect patterns, TypeScript, and performance.", + "description": "React design philosophy for abstraction design, hook review, and hook writing. Covers declarative APIs, lifecycle safety, SSR, state design, effect patterns, TypeScript, and performance.", "author": { "name": "kimyouknow" }, "homepage": "https://react-simplikit.slash.page", "repository": "https://github.com/kimyouknow/react-simplikit", "license": "MIT", - "keywords": ["react", "hooks", "philosophy", "code-review", "ssr", "typescript"] + "keywords": ["react", "hooks", "philosophy", "code-review", "api-design", "ssr", "typescript"] } diff --git a/packages/plugin/README.md b/packages/plugin/README.md index a0a79443..8d062f23 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -1,6 +1,6 @@ # react-hook-philosophy -React hook design philosophy plugin for Claude Code. Two skills for reviewing and writing hooks with principled patterns. +React design philosophy plugin for Claude Code. Includes skills for reviewing and writing hooks, plus a higher-level React abstraction guide. ## Install @@ -12,6 +12,15 @@ claude plugin install --source git-subdir \ ## Skills +### /react-design-principles + +Design React APIs and abstractions in a React-like way. + +- Declarative interfaces over orchestration-heavy helpers +- Lifecycle-safe abstractions instead of generic lifecycle wrappers +- Minimal surfaces, zero-dependency bias, type safety, and documentation +- Best used for library design, API review, and deciding whether an abstraction should exist + ### /react-hook-review Review hooks against 31 design principles. Structured feedback with severity levels. @@ -33,6 +42,7 @@ Write hooks following design philosophy. Themed guide with code examples. | Category | Count | Examples | |----------|-------|---------| +| React design | 4 | Declarative interface, lifecycle respect, minimal surfaces, reliability | | Coding (C1-C14) | 14 | Always return objects, SSR-safe init, no `any`, cleanup | | State Design (U1-U7) | 7 | Derive don't sync, useRef for non-rendered, discriminated unions | | Effect Usage (U8-U14) | 7 | Effects for sync only, no chains, key reset, async cleanup | diff --git a/packages/plugin/skills/react-design-principles/SKILL.md b/packages/plugin/skills/react-design-principles/SKILL.md new file mode 100644 index 00000000..c633bcc1 --- /dev/null +++ b/packages/plugin/skills/react-design-principles/SKILL.md @@ -0,0 +1,56 @@ +--- +name: react-design-principles +description: Design React APIs and abstractions in a React-like way. Covers declarative interfaces, lifecycle-safe abstractions, minimal surfaces, zero-dependency bias, type safety, and documentation. +--- + +# React Design Principles + +Use this skill for higher-level React API decisions: when to extract an abstraction, whether an API is declarative enough, or whether a helper fights React's model. + +If the task becomes a specific custom hook implementation or review, pair this with `react-hook-writing` or `react-hook-review`. + +## 1. Prefer Declarative Interfaces + +React works best when APIs describe desired state, not orchestration steps. + +- Prefer abstractions that remove repeated state + effect coordination from components. +- If consumers must manually wire many states, handlers, and effects every time, the abstraction is probably too low-level. +- Favor APIs that read like intent, not procedure. + +## 2. Respect React's Lifecycle + +Do not introduce abstractions that fight React's lifecycle model. + +- Avoid lifecycle-wrapper helpers such as `useMount`, `useEffectOnce`, or `useLifecycles` unless there is a concrete external synchronization need. +- Prefer purpose-specific abstractions like `useWindowSize` or `useOnlineStatus` over generic lifecycle wrappers. +- When synchronizing with browser or network state, keep setup and cleanup explicit. + +## 3. Keep the Surface Small + +React already provides minimal primitives. Additional abstractions should stay composable and lightweight. + +- Prefer a small API surface over feature-heavy helpers. +- Avoid runtime dependencies unless they remove substantial complexity. +- Inject external clients or fetchers instead of hard-coupling them inside the abstraction. + +## 4. Design for Reliability + +Good React abstractions are predictable in SSR, testable in isolation, and clear at the type level. + +- Prefer SSR-safe initialization for shared hooks and browser-dependent logic. +- Use TypeScript generics and avoid `any`. +- Document the public API with JSDoc so usage stays discoverable. +- Treat cleanup, tests, and edge cases as part of the API design. + +## 5. Review Questions + +Use these questions before finalizing an abstraction: + +1. Does this make components more declarative, or just hide imperative code? +2. Does it respect React's existing lifecycle instead of recreating it? +3. Is the API minimal, typed, and easy to extend? +4. Would the abstraction still make sense without project-specific helpers? + +## Reference + +See [principles.md](references/principles.md) for the detailed rationale behind these principles. diff --git a/packages/plugin/skills/react-design-principles/references/principles.md b/packages/plugin/skills/react-design-principles/references/principles.md new file mode 100644 index 00000000..08b2ca0e --- /dev/null +++ b/packages/plugin/skills/react-design-principles/references/principles.md @@ -0,0 +1,44 @@ +# React-Like Abstraction Principles + +These notes capture the higher-level React ideas behind the plugin skills. + +## Declarative Interface + +Function components and hooks made it possible to extract stateful logic that was previously tangled across lifecycle methods. Good abstractions keep that declarative feel. + +- Prefer APIs that let components state intent. +- Avoid forcing every consumer to coordinate several pieces of local state, refs, handlers, and effects by hand. +- If an abstraction makes the component easier to read at a glance, it is usually moving in the right direction. + +## Respect Lifecycle Without Interference + +React already owns render timing and effect timing. Abstractions should cooperate with that model, not simulate a second lifecycle system on top of it. + +- Avoid generic lifecycle wrappers that encourage "run on mount" thinking over synchronization thinking. +- Prefer abstractions tied to a concrete problem domain. +- Keep setup and cleanup visible whenever external systems are involved. + +## Minimal Interfaces Win + +React exposes a relatively small surface area. Library APIs layered on top of React should usually stay small too. + +- Default to fewer parameters and fewer return fields. +- Add options only when they unlock a real use case. +- Prefer composable building blocks over all-in-one helpers. + +## Reliability Is Part of Design + +A React abstraction is not finished when it "works once." + +- SSR behavior matters for shared hooks and libraries. +- Type safety matters because APIs are reused across many call sites. +- Documentation matters because consumers need to understand the contract quickly. +- Cleanup matters because external synchronization is where subtle bugs and leaks appear. + +## Zero-Dependency Bias + +Dependencies add bundle weight, version pressure, and replacement cost. Reach for them only when they solve more than they introduce. + +- Prefer peer dependencies over runtime dependencies for shared React packages. +- Inject external clients rather than importing them inside hooks when practical. +- Keep abstractions portable across projects and environments. diff --git a/packages/plugin/skills/react-hook-review/SKILL.md b/packages/plugin/skills/react-hook-review/SKILL.md index edf69257..619fecd7 100644 --- a/packages/plugin/skills/react-hook-review/SKILL.md +++ b/packages/plugin/skills/react-hook-review/SKILL.md @@ -1,4 +1,5 @@ --- +name: react-hook-review description: Review React hooks against design philosophy. Checks return values, SSR safety, state design, effect usage, TypeScript patterns, and performance. --- @@ -6,6 +7,8 @@ description: Review React hooks against design philosophy. Checks return values, Review hooks against coding principles and usage patterns. Report findings by severity. +Treat C1, C7, and C14 as opinionated conventions unless the target codebase explicitly adopts them. Report them as stronger findings when the repository standard is clear; otherwise phrase them as consistency recommendations. + ## Coding Principles Checklist ### Required (11 items) @@ -15,6 +18,7 @@ Review hooks against coding principles and usage patterns. Report findings by se 2. **SSR-safe init (C2)** — `useState(FIXED)` + `useEffect(sync)`. No browser API in initializer. Why: Server has no `window` — crashes or hydration mismatch. + Note: For explicitly client-only hooks, a guarded lazy initializer can be acceptable. 3. **Cleanup (C3)** — Every useEffect with side effects returns cleanup (listeners, timers, AbortController). Why: Memory leaks. StrictMode double-mount exposes missing cleanup immediately. @@ -82,6 +86,13 @@ Review hooks against coding principles and usage patterns. Report findings by se - **Extract logic, not lifecycle (U17)** — No `useMount`. Purpose-specific hooks only. +## Review Heuristics + +- Flag React guidance from U1-U17 as behavior or maintainability issues first. +- Flag C1 and C7 as API consistency issues unless the repo treats them as hard requirements. +- Lower the severity of C14 unless debugging quality is materially affected. +- When a hook mirrors props, chains effects, or hides lifecycle wrappers, explain the runtime consequence, not just the rule number. + ## Output Format ### Great Work diff --git a/packages/plugin/skills/react-hook-writing/SKILL.md b/packages/plugin/skills/react-hook-writing/SKILL.md index 6a361b1d..11b9909b 100644 --- a/packages/plugin/skills/react-hook-writing/SKILL.md +++ b/packages/plugin/skills/react-hook-writing/SKILL.md @@ -1,4 +1,5 @@ --- +name: react-hook-writing description: Write React hooks following design philosophy. Covers naming, return values, SSR safety, state design, effect patterns, TypeScript, and performance. --- @@ -6,6 +7,8 @@ description: Write React hooks following design philosophy. Covers naming, retur Design principles for writing React hooks. Each section covers What + Why. +Treat C1 and C7 as project conventions rather than universal React rules. React itself allows tuple returns and positional arguments, but this philosophy prefers objects for extensibility and self-documenting APIs. + ## 1. API Design **Return values (C1):** Always return objects. Even single values use `{ value }`. @@ -52,6 +55,8 @@ const fullName = firstName + ' ' + lastName; **IDs not objects (U6), group related state (U7).** +**Guard clauses (C8):** Prefer early returns over nested conditionals so the happy path stays flat and the failure path is obvious. + ## 4. Effect Patterns **Effects for sync only (U8).** External systems (network, DOM, browser APIs). Not for event handling or data transforms. @@ -66,6 +71,8 @@ const fullName = firstName + ' ' + lastName; **useSyncExternalStore (U12):** For browser API or third-party store subscriptions, prefer `useSyncExternalStore` over `useState` + `useEffect`. Prevents tearing in concurrent rendering and supports SSR server snapshots. +**Named useEffect functions (C14):** Optional but recommended for stack traces and debugging clarity. + ## 5. Cleanup (C3) Every side effect needs cleanup. Three patterns: @@ -122,3 +129,4 @@ Apply only to >30 events/sec (scroll, resize, keyboard): ## Reference See [patterns.md](references/patterns.md) for 3 complete hook implementations. +Use `react-design-principles` when the question is about higher-level React API or abstraction design rather than a specific hook implementation. From dbfb77705776721dd91c119dd0d965c9f02f0f1b Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 21 Apr 2026 18:58:32 +0900 Subject: [PATCH 07/14] =?UTF-8?q?fix(plugin):=20address=20P0=20review=20it?= =?UTF-8?q?ems=20=E2=80=94=20URL,=20LICENSE,=20count=20drift,=20U4=20dangl?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plugin.json + README install URL: kimyouknow -> toss (addresses zztnrudzz13 review) - LICENSE: align copyright with root repo (Viva Republica, Inc) - Remove dangling U4 (useReducer) reference from react-hook-writing/SKILL.md after U4 was removed from usage patterns - README principle counts: 31 -> 30, "17 usage patterns" -> "16 (U4 excluded)", State Design 7 -> 6 - react-design-principles SKILL.md: promote Zero-Dependency Bias to section 5 to match references/principles.md; React design count 4 -> 5 - hook-design-principles.md Opinionated breakdown: "13 + 1" -> "11 + 3 (C1, C7, C14)" to match inline markings - hook-design-principles.md section 3: "17 patterns" -> "16 patterns (U4 removed)" - Korean docs: mark C1/C7 inline 🟡 for parity with EN, sync date to 2026-04-21 --- docs/hook-design-principles.md | 12 +++++++----- docs/ko/hook-design-principles.md | 16 +++++++++------- packages/plugin/.claude-plugin/plugin.json | 2 +- packages/plugin/LICENSE | 2 +- packages/plugin/README.md | 10 +++++----- .../skills/react-design-principles/SKILL.md | 12 +++++++++++- .../plugin/skills/react-hook-writing/SKILL.md | 2 -- 7 files changed, 34 insertions(+), 22 deletions(-) diff --git a/docs/hook-design-principles.md b/docs/hook-design-principles.md index 4892b87a..7b6b9336 100644 --- a/docs/hook-design-principles.md +++ b/docs/hook-design-principles.md @@ -1,6 +1,6 @@ # React Hook Design Principles -> Last Updated: 2026-04-07 +> Last Updated: 2026-04-21 > Status: Draft (pending discussion) > Korean version: [ko/hook-design-principles.md](./ko/hook-design-principles.md) @@ -49,7 +49,9 @@ Hook design philosophy accumulated from operating react-simplikit is defined as Coding style principles extracted from CLAUDE.md + AGENTS.md + internal skills. -### 🟢 Best Practice (13) +### 🟢 Best Practice (11) + 🟡 Opinionated (3: C1, C7, C14) + +> C1 and C7 are marked 🟡 inline because they are project conventions rather than React-wide best practices. C14 is listed in its own 🟡 section below. #### C1. Always Return Objects 🟡 @@ -224,7 +226,7 @@ function useFetch(fetcher: (url: string) => Promise, url: string) { ... } function useFetch(url: string) { const res = await axios.get(url); ... } ``` -### 🟡 Opinionated (1) +### 🟡 Opinionated (C14) #### C14. Named useEffect Functions @@ -244,11 +246,11 @@ function useFetch(url: string) { const res = await axios.get(url); ... } > Separate document: [react-hook-usage-patterns.md](./react-hook-usage-patterns.md) -17 patterns based on React official docs (react.dev), with source URLs and quotes (U1-U17): +16 patterns based on React official docs (react.dev), with source URLs and quotes (U1-U17, U4 removed): | Category | Count | Key Patterns | |----------|-------|-------------| -| State Design | U1-U7 | Derive don't sync, don't mirror props, useRef, discriminated unions, group state | +| State Design | U1-U3, U5-U7 (6) | Derive don't sync, don't mirror props, useRef, discriminated unions, group state | | Effect Usage | U8-U14 | Effects for sync only, no chains, key reset, async cleanup | | Memoization | U15-U16 | useMemo >= 1ms, useCallback + memo() only | | Hook Design | U17 | No lifecycle wrappers, extract reusable stateful logic only | diff --git a/docs/ko/hook-design-principles.md b/docs/ko/hook-design-principles.md index 968a102c..98aeb45c 100644 --- a/docs/ko/hook-design-principles.md +++ b/docs/ko/hook-design-principles.md @@ -1,6 +1,6 @@ # React Hook Design Principles -> 최종 업데이트: 2026-04-03 +> 최종 업데이트: 2026-04-21 > 상태: Draft (논의 후 확정) --- @@ -48,9 +48,11 @@ react-simplikit을 운영하며 축적한 훅 설계 철학을 **하나의 공 CLAUDE.md + AGENTS.md + 내부 스킬에서 추출한 **코딩 스타일** 원칙. -### 🟢 Best Practice (13개) +### 🟢 Best Practice (11개) + 🟡 Opinionated (3개: C1, C7, C14) -#### C1. 항상 객체 반환 +> C1과 C7은 React 전역 베스트 프랙티스가 아니라 프로젝트 컨벤션이므로 인라인 🟡로 표기. C14는 아래 별도 🟡 섹션에 위치. + +#### C1. 항상 객체 반환 🟡 반환값이 1개여도 `{ value }` 형태. 객체는 순서 무관, 이름으로 의미 전달, 확장 시 breaking change 없음. @@ -131,7 +133,7 @@ const controlled = valueProp !== undefined; // ✅ 구분 필요할 때 if (count) { ... } // ❌ count=0 통과 못함 ``` -#### C7. Parameter는 객체로 받기 +#### C7. Parameter는 객체로 받기 🟡 훅의 인자를 개별 파라미터 대신 객체(props)로. 순서 무관 + 이름으로 의미 전달 + 확장 시 breaking change 없음. @@ -213,7 +215,7 @@ function useFetch(fetcher: (url: string) => Promise, url: string) { ... } function useFetch(url: string) { const res = await axios.get(url); ... } ``` -### 🟡 Opinionated (1개) +### 🟡 Opinionated (C14) #### C14. Named useEffect Functions @@ -233,11 +235,11 @@ function useFetch(url: string) { const res = await axios.get(url); ... } > 별도 문서: [react-hook-usage-patterns.md](./react-hook-usage-patterns.md) -React 공식 문서(react.dev) 기반 17개 패턴 (U1-U17): +React 공식 문서(react.dev) 기반 16개 패턴 (U1-U17, U4 제거): | 카테고리 | 개수 | 핵심 | |----------|------|------| -| State Design | U1-U7 | 파생값 계산, props 복사 금지, useRef, union type, state 그룹화 | +| State Design | U1-U3, U5-U7 (6개) | 파생값 계산, props 복사 금지, useRef, union type, state 그룹화 | | Effect Usage | U8-U14 | effect는 외부 동기화 전용, 체인 금지, key 리셋, 비동기 cleanup | | Memoization | U15-U16 | useMemo 1ms+, useCallback + memo() 조합만 | | Hook Design | U17 | lifecycle wrapper 금지, 구체적 목적 훅만 | diff --git a/packages/plugin/.claude-plugin/plugin.json b/packages/plugin/.claude-plugin/plugin.json index 89a2d741..1c6fcbb0 100644 --- a/packages/plugin/.claude-plugin/plugin.json +++ b/packages/plugin/.claude-plugin/plugin.json @@ -4,7 +4,7 @@ "description": "React design philosophy for abstraction design, hook review, and hook writing. Covers declarative APIs, lifecycle safety, SSR, state design, effect patterns, TypeScript, and performance.", "author": { "name": "kimyouknow" }, "homepage": "https://react-simplikit.slash.page", - "repository": "https://github.com/kimyouknow/react-simplikit", + "repository": "https://github.com/toss/react-simplikit", "license": "MIT", "keywords": ["react", "hooks", "philosophy", "code-review", "api-design", "ssr", "typescript"] } diff --git a/packages/plugin/LICENSE b/packages/plugin/LICENSE index 5737c5a3..46fb31ec 100644 --- a/packages/plugin/LICENSE +++ b/packages/plugin/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 kimyouknow +Copyright (c) 2026 Viva Republica, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/packages/plugin/README.md b/packages/plugin/README.md index 8d062f23..a248fca9 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -6,7 +6,7 @@ React design philosophy plugin for Claude Code. Includes skills for reviewing an ```bash claude plugin install --source git-subdir \ - --url https://github.com/kimyouknow/react-simplikit.git \ + --url https://github.com/toss/react-simplikit.git \ --path packages/plugin ``` @@ -23,10 +23,10 @@ Design React APIs and abstractions in a React-like way. ### /react-hook-review -Review hooks against 31 design principles. Structured feedback with severity levels. +Review hooks against 30 design principles. Structured feedback with severity levels. - 14 coding principles (C1-C14): return values, SSR safety, TypeScript, cleanup, performance -- 17 usage patterns (U1-U17): state design, effect usage, memoization, hook design +- 16 usage patterns (U1-U17, excluding U4): state design, effect usage, memoization, hook design - Output: Great Work / Required Changes / Suggestions / Next Steps ### /react-hook-writing @@ -42,9 +42,9 @@ Write hooks following design philosophy. Themed guide with code examples. | Category | Count | Examples | |----------|-------|---------| -| React design | 4 | Declarative interface, lifecycle respect, minimal surfaces, reliability | +| React design | 5 | Declarative interface, lifecycle respect, minimal surfaces, reliability, zero-dependency bias | | Coding (C1-C14) | 14 | Always return objects, SSR-safe init, no `any`, cleanup | -| State Design (U1-U7) | 7 | Derive don't sync, useRef for non-rendered, discriminated unions | +| State Design (U1-U3, U5-U7) | 6 | Derive don't sync, useRef for non-rendered, discriminated unions | | Effect Usage (U8-U14) | 7 | Effects for sync only, no chains, key reset, async cleanup | | Memoization (U15-U16) | 2 | useMemo >= 1ms, useCallback + memo() only | | Hook Design (U17) | 1 | Extract reusable logic, not lifecycle wrappers | diff --git a/packages/plugin/skills/react-design-principles/SKILL.md b/packages/plugin/skills/react-design-principles/SKILL.md index c633bcc1..2037af03 100644 --- a/packages/plugin/skills/react-design-principles/SKILL.md +++ b/packages/plugin/skills/react-design-principles/SKILL.md @@ -42,7 +42,16 @@ Good React abstractions are predictable in SSR, testable in isolation, and clear - Document the public API with JSDoc so usage stays discoverable. - Treat cleanup, tests, and edge cases as part of the API design. -## 5. Review Questions +## 5. Zero-Dependency Bias + +Dependencies add bundle weight, version pressure, and replacement cost. + +- Reach for dependencies only when they solve more than they introduce. +- Prefer `peerDependencies` over runtime dependencies for shared React packages. +- Inject external clients rather than importing them inside hooks when practical. +- Keep abstractions portable across projects and environments. + +## 6. Review Questions Use these questions before finalizing an abstraction: @@ -50,6 +59,7 @@ Use these questions before finalizing an abstraction: 2. Does it respect React's existing lifecycle instead of recreating it? 3. Is the API minimal, typed, and easy to extend? 4. Would the abstraction still make sense without project-specific helpers? +5. Does it avoid unnecessary runtime dependencies? ## Reference diff --git a/packages/plugin/skills/react-hook-writing/SKILL.md b/packages/plugin/skills/react-hook-writing/SKILL.md index 11b9909b..cda6860e 100644 --- a/packages/plugin/skills/react-hook-writing/SKILL.md +++ b/packages/plugin/skills/react-hook-writing/SKILL.md @@ -47,8 +47,6 @@ const fullName = firstName + ' ' + lastName; **useRef for non-rendered values (U3):** Interval IDs, flags, previous values. -**useReducer for complex state (U4):** 3+ related states changing together. - **Discriminated unions (U5):** Replace boolean combos with `type Status = 'idle' | 'loading' | 'done'`. **Don't mirror props (U2):** Use directly, or name `initialX`. From ea972cb226c5947e395466670b6722ec918cba5f Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 21 Apr 2026 20:24:45 +0900 Subject: [PATCH 08/14] refactor(plugin): rename react-hook-philosophy -> react-design-philosophy Reflects broader scope after the react-design-principles skill was added. The plugin now covers higher-level React API design (declarative interfaces, lifecycle safety, minimal surface, reliability, zero-dependency bias) in addition to hook review and writing. --- packages/plugin/.claude-plugin/plugin.json | 2 +- packages/plugin/README.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/plugin/.claude-plugin/plugin.json b/packages/plugin/.claude-plugin/plugin.json index 1c6fcbb0..ec2e6059 100644 --- a/packages/plugin/.claude-plugin/plugin.json +++ b/packages/plugin/.claude-plugin/plugin.json @@ -1,5 +1,5 @@ { - "name": "react-hook-philosophy", + "name": "react-design-philosophy", "version": "0.1.0", "description": "React design philosophy for abstraction design, hook review, and hook writing. Covers declarative APIs, lifecycle safety, SSR, state design, effect patterns, TypeScript, and performance.", "author": { "name": "kimyouknow" }, diff --git a/packages/plugin/README.md b/packages/plugin/README.md index a248fca9..d57e1774 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -1,7 +1,9 @@ -# react-hook-philosophy +# react-design-philosophy React design philosophy plugin for Claude Code. Includes skills for reviewing and writing hooks, plus a higher-level React abstraction guide. +> Renamed from `react-hook-philosophy` to reflect broader scope after the `react-design-principles` skill was added. + ## Install ```bash From 1dea1021225637b695bad4dd1cecacfb529a1129 Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 21 Apr 2026 20:26:07 +0900 Subject: [PATCH 09/14] docs(plugin): remove pre-release rename note from README The plugin has not been published yet, so documenting the pre-release name change is unnecessary noise. The rename is recorded in git history. --- packages/plugin/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/plugin/README.md b/packages/plugin/README.md index d57e1774..7588bebe 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -2,8 +2,6 @@ React design philosophy plugin for Claude Code. Includes skills for reviewing and writing hooks, plus a higher-level React abstraction guide. -> Renamed from `react-hook-philosophy` to reflect broader scope after the `react-design-principles` skill was added. - ## Install ```bash From 934b39f2b534911727c165b4b14c4aa64ab96b5c Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 21 Apr 2026 22:08:51 +0900 Subject: [PATCH 10/14] chore(plugin): add Sookyung (zztnrudzz13) as co-author Credits the contributor who added the react-design-principles skill. --- packages/plugin/.claude-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin/.claude-plugin/plugin.json b/packages/plugin/.claude-plugin/plugin.json index ec2e6059..80bd731d 100644 --- a/packages/plugin/.claude-plugin/plugin.json +++ b/packages/plugin/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "react-design-philosophy", "version": "0.1.0", "description": "React design philosophy for abstraction design, hook review, and hook writing. Covers declarative APIs, lifecycle safety, SSR, state design, effect patterns, TypeScript, and performance.", - "author": { "name": "kimyouknow" }, + "author": { "name": "kimyouknow, Sookyung" }, "homepage": "https://react-simplikit.slash.page", "repository": "https://github.com/toss/react-simplikit", "license": "MIT", From 4bf6593cedc149e9e1f56cc9c640ebf07dc03dad Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 21 Apr 2026 22:10:54 +0900 Subject: [PATCH 11/14] docs: remove Last Updated lines and fix residual U4 count drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop "Last Updated" / "최종 업데이트" header lines from all four doc files (git history is authoritative for update dates) - Fix remaining "17 principles" / "17개 원칙" drift in usage-patterns intro lines to match the U4-removed count (16) --- docs/hook-design-principles.md | 1 - docs/ko/hook-design-principles.md | 1 - docs/ko/react-hook-usage-patterns.md | 3 +-- docs/react-hook-usage-patterns.md | 3 +-- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/hook-design-principles.md b/docs/hook-design-principles.md index 7b6b9336..0caede53 100644 --- a/docs/hook-design-principles.md +++ b/docs/hook-design-principles.md @@ -1,6 +1,5 @@ # React Hook Design Principles -> Last Updated: 2026-04-21 > Status: Draft (pending discussion) > Korean version: [ko/hook-design-principles.md](./ko/hook-design-principles.md) diff --git a/docs/ko/hook-design-principles.md b/docs/ko/hook-design-principles.md index 98aeb45c..37f5b6f5 100644 --- a/docs/ko/hook-design-principles.md +++ b/docs/ko/hook-design-principles.md @@ -1,6 +1,5 @@ # React Hook Design Principles -> 최종 업데이트: 2026-04-21 > 상태: Draft (논의 후 확정) --- diff --git a/docs/ko/react-hook-usage-patterns.md b/docs/ko/react-hook-usage-patterns.md index e7157bfd..dfbd8e37 100644 --- a/docs/ko/react-hook-usage-patterns.md +++ b/docs/ko/react-hook-usage-patterns.md @@ -1,10 +1,9 @@ # React Hook Usage Patterns -> 최종 업데이트: 2026-04-03 > 출처: React 공식 문서 (react.dev) > 관련: [Hook Design Principles](./hook-design-principles.md) -코딩 스타일이 아닌 **hooks를 올바르게 사용하는 패턴**. 17개 원칙. +코딩 스타일이 아닌 **hooks를 올바르게 사용하는 패턴**. 16개 원칙 (U1-U17, U4 제거). --- diff --git a/docs/react-hook-usage-patterns.md b/docs/react-hook-usage-patterns.md index b45d7aab..48b9a28d 100644 --- a/docs/react-hook-usage-patterns.md +++ b/docs/react-hook-usage-patterns.md @@ -1,11 +1,10 @@ # React Hook Usage Patterns -> Last Updated: 2026-04-07 > Source: React official documentation (react.dev) > Related: [Hook Design Principles](./hook-design-principles.md) > Korean version: [ko/react-hook-usage-patterns.md](./ko/react-hook-usage-patterns.md) -Patterns for **correctly using hooks** — not coding style, but React-specific best practices. 17 principles. +Patterns for **correctly using hooks** — not coding style, but React-specific best practices. 16 principles (U1-U17, U4 removed). --- From 7022e4af0857d0929aa31c6da4c89c25d035e7bd Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 21 Apr 2026 22:12:39 +0900 Subject: [PATCH 12/14] chore(plugin): use zztnrudzz13 GitHub handle instead of Sookyung The GitHub login matches the git commit author identifier, so it's unambiguous and ties directly to commit history. --- packages/plugin/.claude-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin/.claude-plugin/plugin.json b/packages/plugin/.claude-plugin/plugin.json index 80bd731d..5d3cbb70 100644 --- a/packages/plugin/.claude-plugin/plugin.json +++ b/packages/plugin/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "react-design-philosophy", "version": "0.1.0", "description": "React design philosophy for abstraction design, hook review, and hook writing. Covers declarative APIs, lifecycle safety, SSR, state design, effect patterns, TypeScript, and performance.", - "author": { "name": "kimyouknow, Sookyung" }, + "author": { "name": "kimyouknow, zztnrudzz13" }, "homepage": "https://react-simplikit.slash.page", "repository": "https://github.com/toss/react-simplikit", "license": "MIT", From 73da09b81376502194f2645ca7076968f138edf4 Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 21 Apr 2026 22:25:59 +0900 Subject: [PATCH 13/14] style: apply prettier formatting to plugin docs Resolves CI 'quality (format)' and 'autofix' failures. All 8 files flagged by prettier check are now formatted consistently. --- docs/hook-design-principles.md | 188 ++++++++++-------- docs/ko/hook-design-principles.md | 185 +++++++++-------- docs/ko/react-hook-usage-patterns.md | 17 +- docs/react-hook-usage-patterns.md | 65 +++--- packages/plugin/README.md | 16 +- .../plugin/skills/react-hook-review/SKILL.md | 6 +- .../plugin/skills/react-hook-writing/SKILL.md | 10 +- .../react-hook-writing/references/patterns.md | 149 ++++++++------ 8 files changed, 353 insertions(+), 283 deletions(-) diff --git a/docs/hook-design-principles.md b/docs/hook-design-principles.md index 0caede53..8b44c042 100644 --- a/docs/hook-design-principles.md +++ b/docs/hook-design-principles.md @@ -16,31 +16,31 @@ Hook design philosophy accumulated from operating react-simplikit is defined as ### Two Directions of Principles -| Direction | Source | Scope | -|-----------|--------|-------| -| **Coding Principles** (Section 2) | CLAUDE.md, AGENTS.md, internal skills | Return values, TypeScript, performance, documentation | -| **Usage Patterns** (Section 3) | React official docs (react.dev) | State design, effect usage, memoization, custom hook design | +| Direction | Source | Scope | +| --------------------------------- | ------------------------------------- | ----------------------------------------------------------- | +| **Coding Principles** (Section 2) | CLAUDE.md, AGENTS.md, internal skills | Return values, TypeScript, performance, documentation | +| **Usage Patterns** (Section 3) | React official docs (react.dev) | State design, effect usage, memoization, custom hook design | ### Core Requirements -| # | Requirement | Detail | -|---|------------|--------| -| R1 | Shared principles for review/writing | Both skills reference the same principles | -| R2 | Why-first | Not just rules (What), but philosophy (Why) with narrative explanation | -| R3 | Opinionated transparency | Clearly mark 🟢 Best Practice vs 🟡 Opinionated | -| R4 | Project-agnostic | No react-simplikit paths/commands/utils — universal principles only | -| R5 | Cross-tool | Claude Code plugin + Codex (AGENTS.md) + Cursor (.cursorrules) | +| # | Requirement | Detail | +| --- | ------------------------------------ | ---------------------------------------------------------------------- | +| R1 | Shared principles for review/writing | Both skills reference the same principles | +| R2 | Why-first | Not just rules (What), but philosophy (Why) with narrative explanation | +| R3 | Opinionated transparency | Clearly mark 🟢 Best Practice vs 🟡 Opinionated | +| R4 | Project-agnostic | No react-simplikit paths/commands/utils — universal principles only | +| R5 | Cross-tool | Claude Code plugin + Codex (AGENTS.md) + Cursor (.cursorrules) | ### Open Questions -| # | Question | Options | -|---|---------|---------| -| Q1 | Include C14 (Named useEffect)? | A) Include as "Recommended" B) Exclude | -| Q2 | Recommend C2 (SSR-Safe) for non-SSR projects? | A) Always B) SSR projects only | -| Q3 | Require @example in C9 (JSDoc)? | A) All 4 tags required B) @example is recommended | -| Q4 | Any additional principles? | — | -| Q5 | Finalize principles first, or go straight to plugin structure? | A) Principles first B) Plugin directly | -| Q6 | Plugin distribution channel | A) git-subdir B) npm C) TBD | +| # | Question | Options | +| --- | -------------------------------------------------------------- | ------------------------------------------------- | +| Q1 | Include C14 (Named useEffect)? | A) Include as "Recommended" B) Exclude | +| Q2 | Recommend C2 (SSR-Safe) for non-SSR projects? | A) Always B) SSR projects only | +| Q3 | Require @example in C9 (JSDoc)? | A) All 4 tags required B) @example is recommended | +| Q4 | Any additional principles? | — | +| Q5 | Finalize principles first, or go straight to plugin structure? | A) Principles first B) Plugin directly | +| Q6 | Plugin distribution channel | A) git-subdir B) npm C) TBD | --- @@ -60,9 +60,9 @@ Return objects even for single values — `{ value }` form. Objects are order-in > 📖 [react.dev — Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) ```ts -function useDebounce(value: T, delay: number): { value: T } -function useToggle(init: boolean): { value: boolean; toggle: () => void } -function usePagination(): { page: number; next: () => void; prev: () => void } +function useDebounce(value: T, delay: number): { value: T }; +function useToggle(init: boolean): { value: boolean; toggle: () => void }; +function usePagination(): { page: number; next: () => void; prev: () => void }; ``` #### C2. SSR-Safe Initialization @@ -74,7 +74,9 @@ function usePagination(): { page: number; next: () => void; prev: () => void } ```ts // ✅ SSR safe const [width, setWidth] = useState(0); -useEffect(function syncWidth() { setWidth(window.innerWidth); }, []); +useEffect(function syncWidth() { + setWidth(window.innerWidth); +}, []); // ❌ SSR crash const [width, setWidth] = useState(window.innerWidth); @@ -88,10 +90,9 @@ const [width, setWidth] = useState(() => { #### C3. useEffect Cleanup When Subscribing -Return cleanup when your effect sets up subscriptions, listeners, timers, or ongoing connections. React docs: cleanup is *optional*, not required for every effect — but mandatory when synchronizing with external systems. +Return cleanup when your effect sets up subscriptions, listeners, timers, or ongoing connections. React docs: cleanup is _optional_, not required for every effect — but mandatory when synchronizing with external systems. -> 📖 [react.dev — useEffect](https://react.dev/reference/react/useEffect) -> *"Your setup function may also optionally return a cleanup function."* +> 📖 [react.dev — useEffect](https://react.dev/reference/react/useEffect) > _"Your setup function may also optionally return a cleanup function."_ ```ts // Event listeners @@ -101,11 +102,14 @@ useEffect(function subscribe() { }, []); // AbortController (async) -useEffect(function fetchData() { - const controller = new AbortController(); - fetch(url, { signal: controller.signal }).then(/* ... */); - return () => controller.abort(); -}, [url]); +useEffect( + function fetchData() { + const controller = new AbortController(); + fetch(url, { signal: controller.signal }).then(/* ... */); + return () => controller.abort(); + }, + [url] +); // Timers useEffect(function tick() { @@ -120,7 +124,7 @@ Use generics ``. `any` propagates and defeats the type system. Justified `esl ```ts // ✅ Generic -function useDebounce(value: T, delay: number): T +function useDebounce(value: T, delay: number): T; // ✅ Justified exception (comment required) // eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic callback @@ -149,12 +153,22 @@ Hook params as object props, not positional args. Order-independent, self-docume ```ts // ✅ Object params -function useDebounce({ value, delay, leading }: { - value: T; delay: number; leading?: boolean; -}): { value: T } +function useDebounce({ + value, + delay, + leading, +}: { + value: T; + delay: number; + leading?: boolean; +}): { value: T }; // ❌ Positional params -function useDebounce(value: T, delay: number, leading?: boolean): { value: T } +function useDebounce( + value: T, + delay: number, + leading?: boolean +): { value: T }; ``` #### C8. Guard Clauses (Early Return) @@ -164,13 +178,19 @@ Early return over nested if-else. Filter failure conditions first, keep success ```ts // ✅ function process(value: string | null) { - if (value == null) { return DEFAULT; } + if (value == null) { + return DEFAULT; + } return transform(value); } // ❌ function process(value: string | null) { - if (value != null) { return transform(value); } else { return DEFAULT; } + if (value != null) { + return transform(value); + } else { + return DEFAULT; + } } ``` @@ -193,10 +213,10 @@ All public APIs must have `@description` + `@param` + `@returns` + `@example`. E Apply only to high-frequency events (30+/sec). Not needed for general hooks. -| Technique | When to Apply | -|-----------|--------------| -| Throttle (16ms) | scroll, resize, pointer, keyboard | -| Deduplicate | Skip setState when value unchanged | +| Technique | When to Apply | +| --------------- | ------------------------------------------- | +| Throttle (16ms) | scroll, resize, pointer, keyboard | +| Deduplicate | Skip setState when value unchanged | | startTransition | Non-urgent derived computations (React 18+) | #### C11. Function Keyword for Declarations @@ -204,9 +224,11 @@ Apply only to high-frequency events (30+/sec). Not needed for general hooks. Use `function` keyword for declarations. Arrows only for inline callbacks (map, filter). ```ts -function toggle(state: boolean) { return !state; } // ✅ declaration -items.filter(item => item != null); // ✅ inline -const toggle = (state: boolean) => !state; // ❌ arrow for declaration +function toggle(state: boolean) { + return !state; +} // ✅ declaration +items.filter(item => item != null); // ✅ inline +const toggle = (state: boolean) => !state; // ❌ arrow for declaration ``` #### C12. Zero Runtime Dependencies @@ -233,10 +255,10 @@ function useFetch(url: string) { const res = await axios.get(url); ... } ### Excluded (Project-Specific Decisions) -| Item | Reason | -|------|--------| -| Import extensions (.js/.ts) | Build-tool dependent | -| 100% test coverage | Project policy | +| Item | Reason | +| ----------------------------------- | -------------------------- | +| Import extensions (.js/.ts) | Build-tool dependent | +| 100% test coverage | Project policy | | File structure / commit conventions | Not hook design philosophy | --- @@ -247,12 +269,12 @@ function useFetch(url: string) { const res = await axios.get(url); ... } 16 patterns based on React official docs (react.dev), with source URLs and quotes (U1-U17, U4 removed): -| Category | Count | Key Patterns | -|----------|-------|-------------| +| Category | Count | Key Patterns | +| ------------ | ---------------- | -------------------------------------------------------------------------------- | | State Design | U1-U3, U5-U7 (6) | Derive don't sync, don't mirror props, useRef, discriminated unions, group state | -| Effect Usage | U8-U14 | Effects for sync only, no chains, key reset, async cleanup | -| Memoization | U15-U16 | useMemo >= 1ms, useCallback + memo() only | -| Hook Design | U17 | No lifecycle wrappers, extract reusable stateful logic only | +| Effect Usage | U8-U14 | Effects for sync only, no chains, key reset, async cleanup | +| Memoization | U15-U16 | useMemo >= 1ms, useCallback + memo() only | +| Hook Design | U17 | No lifecycle wrappers, extract reusable stateful logic only | --- @@ -288,51 +310,51 @@ packages/plugin/ (planned) ### Cross-Tool Support -| Tool | File | Current | Planned | -|------|------|---------|---------| -| Claude Code (internal) | `.claude/skills/` | ✅ 10 skills | Keep | -| Claude Code (plugin) | `packages/plugin/` | ❌ | Create via Phase 1-5 | -| Codex | `AGENTS.md` | ✅ 162 lines | Split into Part 1 (Universal) + Part 2 (Project) | -| Cursor | `.cursorrules` | ✅ 28 lines | Keep AGENTS.md reference | +| Tool | File | Current | Planned | +| ---------------------- | ------------------ | ------------ | ------------------------------------------------ | +| Claude Code (internal) | `.claude/skills/` | ✅ 10 skills | Keep | +| Claude Code (plugin) | `packages/plugin/` | ❌ | Create via Phase 1-5 | +| Codex | `AGENTS.md` | ✅ 162 lines | Split into Part 1 (Universal) + Part 2 (Project) | +| Cursor | `.cursorrules` | ✅ 28 lines | Keep AGENTS.md reference | ### Extraction Rules -| Extracted (Philosophy) | Left Behind (Implementation) | -|----------------------|---------------------------| -| "Always return objects" | `packages/core/src/hooks/` paths | -| "Named useEffect improves stack traces" | `yarn test`, `yarn fix` commands | +| Extracted (Philosophy) | Left Behind (Implementation) | +| ------------------------------------------ | ------------------------------------ | +| "Always return objects" | `packages/core/src/hooks/` paths | +| "Named useEffect improves stack traces" | `yarn test`, `yarn fix` commands | | "SSR-safe: fixed initial + useEffect sync" | `renderHookSSR.serverOnly()` utility | -| "4 JSDoc tags for AI doc generation" | `100%` coverage threshold | +| "4 JSDoc tags for AI doc generation" | `100%` coverage threshold | ### Generalization Transforms -| Before (Project-Specific) | After (Universal) | -|---|---| +| Before (Project-Specific) | After (Universal) | +| ---------------------------- | ------------------------------- | | `renderHookSSR.serverOnly()` | Vitest + `delete global.window` | -| `yarn test` / `yarn fix` | "Run your test suite" | -| `packages/core/` paths | "your source directory" | -| `react-simplikit` references | Removed | +| `yarn test` / `yarn fix` | "Run your test suite" | +| `packages/core/` paths | "your source directory" | +| `react-simplikit` references | Removed | --- ## 5. Execution Roadmap -| Phase | Content | Output | -|-------|---------|--------| -| 1 | Directory + plugin.json + README | `packages/plugin/` structure | -| 2 | react-hook-review SKILL.md | C1-C14 + U1-U17 checklist | -| 3 | react-hook-writing SKILL.md + patterns.md | Themed guide + 3 hook examples | -| 4 | Generalization validation (grep) | 0 project references | -| 5 | Plugin validate + local test | Working confirmation | +| Phase | Content | Output | +| ----- | ----------------------------------------- | ------------------------------ | +| 1 | Directory + plugin.json + README | `packages/plugin/` structure | +| 2 | react-hook-review SKILL.md | C1-C14 + U1-U17 checklist | +| 3 | react-hook-writing SKILL.md + patterns.md | Themed guide + 3 hook examples | +| 4 | Generalization validation (grep) | 0 project references | +| 5 | Plugin validate + local test | Working confirmation | ### Validation Criteria -| Item | Pass Criteria | -|------|-------------| -| Plugin structure | `claude plugin validate .` — 0 errors | -| Universality | 0 project-specific references in another React project | -| Philosophy depth | Every rule has narrative "Why" | -| Opinionated transparency | 🟡 items have trade-offs stated | +| Item | Pass Criteria | +| ------------------------ | ------------------------------------------------------ | +| Plugin structure | `claude plugin validate .` — 0 errors | +| Universality | 0 project-specific references in another React project | +| Philosophy depth | Every rule has narrative "Why" | +| Opinionated transparency | 🟡 items have trade-offs stated | ### Future Expansion diff --git a/docs/ko/hook-design-principles.md b/docs/ko/hook-design-principles.md index 37f5b6f5..d07165f5 100644 --- a/docs/ko/hook-design-principles.md +++ b/docs/ko/hook-design-principles.md @@ -15,31 +15,31 @@ react-simplikit을 운영하며 축적한 훅 설계 철학을 **하나의 공 ### 원칙의 두 가지 방향 -| 방향 | 출처 | 범위 | -|------|------|------| -| **훅 코딩 원칙** (Section 2) | CLAUDE.md, AGENTS.md, 내부 스킬 | 반환값, TypeScript, 성능, 문서화 등 코딩 스타일 | -| **훅 사용 패턴** (Section 3) | React 공식 문서 (react.dev) | state 설계, effect 사용법, 메모이제이션, 커스텀 훅 설계 | +| 방향 | 출처 | 범위 | +| ---------------------------- | ------------------------------- | ------------------------------------------------------- | +| **훅 코딩 원칙** (Section 2) | CLAUDE.md, AGENTS.md, 내부 스킬 | 반환값, TypeScript, 성능, 문서화 등 코딩 스타일 | +| **훅 사용 패턴** (Section 3) | React 공식 문서 (react.dev) | state 설계, effect 사용법, 메모이제이션, 커스텀 훅 설계 | ### 핵심 요구사항 -| # | 요구사항 | 상세 | -|---|---------|------| -| R1 | 리뷰/생성 공통 원칙 | 두 스킬이 동일한 원칙 참조 | -| R2 | Why 중심 | 규칙(What)만 나열하지 않고 철학(Why)을 narrative로 설명 | -| R3 | Opinionated 투명성 | 🟢 Best Practice vs 🟡 Opinionated 명시 | -| R4 | 프로젝트 무관 | react-simplikit 경로/명령어/유틸 없이 범용 원칙만 | -| R5 | Cross-tool | Claude Code 플러그인 + Codex(AGENTS.md) + Cursor(.cursorrules) | +| # | 요구사항 | 상세 | +| --- | ------------------- | -------------------------------------------------------------- | +| R1 | 리뷰/생성 공통 원칙 | 두 스킬이 동일한 원칙 참조 | +| R2 | Why 중심 | 규칙(What)만 나열하지 않고 철학(Why)을 narrative로 설명 | +| R3 | Opinionated 투명성 | 🟢 Best Practice vs 🟡 Opinionated 명시 | +| R4 | 프로젝트 무관 | react-simplikit 경로/명령어/유틸 없이 범용 원칙만 | +| R5 | Cross-tool | Claude Code 플러그인 + Codex(AGENTS.md) + Cursor(.cursorrules) | ### 결정 필요 사항 -| # | 질문 | 선택지 | -|---|------|--------| -| Q1 | C14(Named useEffect)를 포함할지? | A) "Recommended"로 포함 B) 제외 | -| Q2 | C2(SSR-Safe)를 비-SSR 프로젝트에도 권장할지? | A) 항상 B) SSR 사용 시만 | -| Q3 | C9(JSDoc)의 @example을 필수로 할지? | A) 4-tag 전부 필수 B) @example은 권장 | -| Q4 | 추가할 원칙이 있는지? | — | -| Q5 | 원칙 먼저 확정할지, 바로 플러그인 구조로 갈지? | A) 원칙 먼저 B) 바로 플러그인 | -| Q6 | 플러그인 배포 채널 | A) git-subdir B) npm C) 미정 | +| # | 질문 | 선택지 | +| --- | ---------------------------------------------- | ------------------------------------- | +| Q1 | C14(Named useEffect)를 포함할지? | A) "Recommended"로 포함 B) 제외 | +| Q2 | C2(SSR-Safe)를 비-SSR 프로젝트에도 권장할지? | A) 항상 B) SSR 사용 시만 | +| Q3 | C9(JSDoc)의 @example을 필수로 할지? | A) 4-tag 전부 필수 B) @example은 권장 | +| Q4 | 추가할 원칙이 있는지? | — | +| Q5 | 원칙 먼저 확정할지, 바로 플러그인 구조로 갈지? | A) 원칙 먼저 B) 바로 플러그인 | +| Q6 | 플러그인 배포 채널 | A) git-subdir B) npm C) 미정 | --- @@ -56,9 +56,9 @@ CLAUDE.md + AGENTS.md + 내부 스킬에서 추출한 **코딩 스타일** 원 반환값이 1개여도 `{ value }` 형태. 객체는 순서 무관, 이름으로 의미 전달, 확장 시 breaking change 없음. ```ts -function useDebounce(value: T, delay: number): { value: T } -function useToggle(init: boolean): { value: boolean; toggle: () => void } -function usePagination(): { page: number; next: () => void; prev: () => void } +function useDebounce(value: T, delay: number): { value: T }; +function useToggle(init: boolean): { value: boolean; toggle: () => void }; +function usePagination(): { page: number; next: () => void; prev: () => void }; ``` #### C2. SSR-Safe 초기화 @@ -68,7 +68,9 @@ function usePagination(): { page: number; next: () => void; prev: () => void } ```ts // ✅ SSR 안전 const [width, setWidth] = useState(0); -useEffect(function syncWidth() { setWidth(window.innerWidth); }, []); +useEffect(function syncWidth() { + setWidth(window.innerWidth); +}, []); // ❌ SSR 크래시 const [width, setWidth] = useState(window.innerWidth); @@ -92,11 +94,14 @@ useEffect(function subscribe() { }, []); // AbortController (비동기) -useEffect(function fetchData() { - const controller = new AbortController(); - fetch(url, { signal: controller.signal }).then(/* ... */); - return () => controller.abort(); -}, [url]); +useEffect( + function fetchData() { + const controller = new AbortController(); + fetch(url, { signal: controller.signal }).then(/* ... */); + return () => controller.abort(); + }, + [url] +); // 타이머 useEffect(function tick() { @@ -111,7 +116,7 @@ useEffect(function tick() { ```ts // ✅ Generic -function useDebounce(value: T, delay: number): T +function useDebounce(value: T, delay: number): T; // ✅ 정당한 예외 (코멘트 필수) // eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic callback @@ -138,12 +143,22 @@ if (count) { ... } // ❌ count=0 통과 못함 ```ts // ✅ 객체 -function useDebounce({ value, delay, leading }: { - value: T; delay: number; leading?: boolean; -}): { value: T } +function useDebounce({ + value, + delay, + leading, +}: { + value: T; + delay: number; + leading?: boolean; +}): { value: T }; // ❌ 위치 기반 -function useDebounce(value: T, delay: number, leading?: boolean): { value: T } +function useDebounce( + value: T, + delay: number, + leading?: boolean +): { value: T }; ``` #### C8. Guard Clauses (Early Return) @@ -153,13 +168,19 @@ nested if-else 대신 early return. 실패 조건 먼저 걸러내고 성공 로 ```ts // ✅ function process(value: string | null) { - if (value == null) { return DEFAULT; } + if (value == null) { + return DEFAULT; + } return transform(value); } // ❌ function process(value: string | null) { - if (value != null) { return transform(value); } else { return DEFAULT; } + if (value != null) { + return transform(value); + } else { + return DEFAULT; + } } ``` @@ -182,20 +203,22 @@ function process(value: string | null) { 고빈도(30+/sec) 이벤트에만 적용. 일반 훅에는 불필요. -| 기법 | 적용 시점 | -|------|-----------| +| 기법 | 적용 시점 | +| --------------- | --------------------------------- | | Throttle (16ms) | scroll, resize, pointer, keyboard | -| Deduplicate | 값 미변경 시 setState skip | -| startTransition | 비긴급 파생 계산 (React 18+) | +| Deduplicate | 값 미변경 시 setState skip | +| startTransition | 비긴급 파생 계산 (React 18+) | #### C11. Function Keyword for Declarations 함수 선언은 `function` 키워드. 화살표는 인라인 콜백(map, filter)에만. ```ts -function toggle(state: boolean) { return !state; } // ✅ 선언 -items.filter(item => item != null); // ✅ 인라인 -const toggle = (state: boolean) => !state; // ❌ 선언에 화살표 +function toggle(state: boolean) { + return !state; +} // ✅ 선언 +items.filter(item => item != null); // ✅ 인라인 +const toggle = (state: boolean) => !state; // ❌ 선언에 화살표 ``` #### C12. Zero Runtime Dependencies @@ -222,11 +245,11 @@ function useFetch(url: string) { const res = await axios.get(url); ... } ### 제외 (프로젝트별 결정) -| 항목 | 이유 | -|------|------| -| Import extensions (.js/.ts) | 빌드 도구 의존적 | -| 100% test coverage | 프로젝트 정책 | -| 파일 구조/커밋 컨벤션 | 훅 설계 철학 아님 | +| 항목 | 이유 | +| --------------------------- | ----------------- | +| Import extensions (.js/.ts) | 빌드 도구 의존적 | +| 100% test coverage | 프로젝트 정책 | +| 파일 구조/커밋 컨벤션 | 훅 설계 철학 아님 | --- @@ -236,12 +259,12 @@ function useFetch(url: string) { const res = await axios.get(url); ... } React 공식 문서(react.dev) 기반 16개 패턴 (U1-U17, U4 제거): -| 카테고리 | 개수 | 핵심 | -|----------|------|------| +| 카테고리 | 개수 | 핵심 | +| ------------ | ------------------ | -------------------------------------------------------------- | | State Design | U1-U3, U5-U7 (6개) | 파생값 계산, props 복사 금지, useRef, union type, state 그룹화 | -| Effect Usage | U8-U14 | effect는 외부 동기화 전용, 체인 금지, key 리셋, 비동기 cleanup | -| Memoization | U15-U16 | useMemo 1ms+, useCallback + memo() 조합만 | -| Hook Design | U17 | lifecycle wrapper 금지, 구체적 목적 훅만 | +| Effect Usage | U8-U14 | effect는 외부 동기화 전용, 체인 금지, key 리셋, 비동기 cleanup | +| Memoization | U15-U16 | useMemo 1ms+, useCallback + memo() 조합만 | +| Hook Design | U17 | lifecycle wrapper 금지, 구체적 목적 훅만 | --- @@ -277,51 +300,51 @@ packages/plugin/ (planned) ### Cross-Tool 지원 -| 도구 | 파일 | 현재 | 변경 | -|------|------|------|------| -| Claude Code (내부) | `.claude/skills/` | ✅ 10개 | 유지 | -| Claude Code (플러그인) | `packages/plugin/` | ❌ | Phase 1-5로 생성 | -| Codex | `AGENTS.md` | ✅ 162줄 | Part 1(Universal) + Part 2(Project) 분리 | -| Cursor | `.cursorrules` | ✅ 28줄 | AGENTS.md 참조 유지 | +| 도구 | 파일 | 현재 | 변경 | +| ---------------------- | ------------------ | -------- | ---------------------------------------- | +| Claude Code (내부) | `.claude/skills/` | ✅ 10개 | 유지 | +| Claude Code (플러그인) | `packages/plugin/` | ❌ | Phase 1-5로 생성 | +| Codex | `AGENTS.md` | ✅ 162줄 | Part 1(Universal) + Part 2(Project) 분리 | +| Cursor | `.cursorrules` | ✅ 28줄 | AGENTS.md 참조 유지 | ### 추출 규칙 -| 추출됨 (철학) | 남겨짐 (구현) | -|--------------|-------------| -| "항상 객체 반환" | `packages/core/src/hooks/` 경로 | -| "Named useEffect improves stack traces" | `yarn test`, `yarn fix` 명령 | +| 추출됨 (철학) | 남겨짐 (구현) | +| ------------------------------------------ | --------------------------------- | +| "항상 객체 반환" | `packages/core/src/hooks/` 경로 | +| "Named useEffect improves stack traces" | `yarn test`, `yarn fix` 명령 | | "SSR-safe: fixed initial + useEffect sync" | `renderHookSSR.serverOnly()` 유틸 | -| "4 JSDoc tags for AI doc generation" | `100%` coverage 기준 | +| "4 JSDoc tags for AI doc generation" | `100%` coverage 기준 | ### 일반화 변환 -| Before (프로젝트 전용) | After (범용) | -|---|---| +| Before (프로젝트 전용) | After (범용) | +| ---------------------------- | ------------------------------- | | `renderHookSSR.serverOnly()` | Vitest + `delete global.window` | -| `yarn test` / `yarn fix` | "Run your test suite" | -| `packages/core/` 경로 | "your source directory" | -| `react-simplikit` 언급 | 제거 | +| `yarn test` / `yarn fix` | "Run your test suite" | +| `packages/core/` 경로 | "your source directory" | +| `react-simplikit` 언급 | 제거 | --- ## 5. 실행 로드맵 -| Phase | 내용 | 산출물 | -|-------|------|--------| -| 1 | 디렉토리 + plugin.json + README | `packages/plugin/` 구조 | -| 2 | react-hook-review SKILL.md | C1-C14 + U1-U17 체크리스트 | -| 3 | react-hook-writing SKILL.md + patterns.md | 테마별 가이드 + 3개 훅 예시 | -| 4 | 일반화 검증 (grep) | 프로젝트 참조 0건 | -| 5 | 플러그인 validate + 로컬 테스트 | 동작 확인 | +| Phase | 내용 | 산출물 | +| ----- | ----------------------------------------- | --------------------------- | +| 1 | 디렉토리 + plugin.json + README | `packages/plugin/` 구조 | +| 2 | react-hook-review SKILL.md | C1-C14 + U1-U17 체크리스트 | +| 3 | react-hook-writing SKILL.md + patterns.md | 테마별 가이드 + 3개 훅 예시 | +| 4 | 일반화 검증 (grep) | 프로젝트 참조 0건 | +| 5 | 플러그인 validate + 로컬 테스트 | 동작 확인 | ### 검증 기준 -| 항목 | 통과 기준 | -|------|---------| -| 플러그인 구조 | `claude plugin validate .` 에러 0 | -| 범용성 | 다른 React 프로젝트에서 프로젝트 참조 0건 | -| 철학 깊이 | 각 규칙의 Why가 narrative | -| Opinionated 투명성 | 🟡 패턴에 trade-off 존재 | +| 항목 | 통과 기준 | +| ------------------ | ----------------------------------------- | +| 플러그인 구조 | `claude plugin validate .` 에러 0 | +| 범용성 | 다른 React 프로젝트에서 프로젝트 참조 0건 | +| 철학 깊이 | 각 규칙의 Why가 narrative | +| Opinionated 투명성 | 🟡 패턴에 trade-off 존재 | ### 향후 확장 diff --git a/docs/ko/react-hook-usage-patterns.md b/docs/ko/react-hook-usage-patterns.md index dfbd8e37..634b0c4e 100644 --- a/docs/ko/react-hook-usage-patterns.md +++ b/docs/ko/react-hook-usage-patterns.md @@ -125,11 +125,18 @@ N개 boolean → 2^N 조합. 단일 status union으로 불가능한 상태 타 fetch/timer/subscription → cleanup 없으면 race condition. 빠른 prop 변경 시 이전 응답이 이후 응답을 덮어씀. ```ts -useEffect(function fetchResults() { - let ignore = false; - fetchAPI(query).then(data => { if (!ignore) setResults(data); }); - return () => { ignore = true; }; -}, [query]); +useEffect( + function fetchResults() { + let ignore = false; + fetchAPI(query).then(data => { + if (!ignore) setResults(data); + }); + return () => { + ignore = true; + }; + }, + [query] +); ``` --- diff --git a/docs/react-hook-usage-patterns.md b/docs/react-hook-usage-patterns.md index 48b9a28d..e9d7c9db 100644 --- a/docs/react-hook-usage-patterns.md +++ b/docs/react-hook-usage-patterns.md @@ -14,8 +14,7 @@ Patterns for **correctly using hooks** — not coding style, but React-specific If a value can be computed from existing props or state, calculate it during render. Syncing with useEffect causes a 1-render delay + unnecessary extra render. -> 📖 [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) -> *"If something can be calculated from the existing props or state, don't put it in state. Instead, calculate it during rendering."* +> 📖 [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) > _"If something can be calculated from the existing props or state, don't put it in state. Instead, calculate it during rendering."_ ```ts // ❌ const [fullName, setFullName] = useState(''); @@ -27,8 +26,7 @@ If a value can be computed from existing props or state, calculate it during ren Copying a prop into useState means parent changes are silently ignored. Use the prop directly, or name it `initialX` if intentional. -> 📖 [Choosing the State Structure — Avoid redundant state](https://react.dev/learn/choosing-the-state-structure#avoid-redundant-state) -> *"If you can calculate some information from the component's props or its existing state variables during rendering, you should not put that information into that component's state."* +> 📖 [Choosing the State Structure — Avoid redundant state](https://react.dev/learn/choosing-the-state-structure#avoid-redundant-state) > _"If you can calculate some information from the component's props or its existing state variables during rendering, you should not put that information into that component's state."_ ```ts // ❌ const [color, setColor] = useState(messageColor); @@ -40,8 +38,7 @@ Copying a prop into useState means parent changes are silently ignored. Use the Interval IDs, previous values, internal flags — use useRef instead of useState. Avoids unnecessary re-renders. Never read/write `ref.current` during rendering. -> 📖 [Referencing Values with Refs](https://react.dev/learn/referencing-values-with-refs) -> *"When you want a component to 'remember' some information, but you don't want that information to trigger new renders, you can use a ref."* +> 📖 [Referencing Values with Refs](https://react.dev/learn/referencing-values-with-refs) > _"When you want a component to 'remember' some information, but you don't want that information to trigger new renders, you can use a ref."_ ```ts // ❌ const [intervalId, setIntervalId] = useState(null); @@ -52,8 +49,7 @@ Interval IDs, previous values, internal flags — use useRef instead of useState N booleans → 2^N combinations with invalid states. A single status union type prevents impossible states at the type level. -> 📖 [Choosing the State Structure — Avoid contradictions in state](https://react.dev/learn/choosing-the-state-structure#avoid-contradictions-in-state) -> *"Since isSending and isSent should never be true at the same time, it is better to replace them with one status state variable."* +> 📖 [Choosing the State Structure — Avoid contradictions in state](https://react.dev/learn/choosing-the-state-structure#avoid-contradictions-in-state) > _"Since isSending and isSent should never be true at the same time, it is better to replace them with one status state variable."_ ```ts // ❌ const [isSending, setIsSending] = useState(false); @@ -66,8 +62,7 @@ N booleans → 2^N combinations with invalid states. A single status union type Copying a selected item from a list into state → stale when source updates. Store the ID and derive during render. -> 📖 [Choosing the State Structure — Avoid duplication in state](https://react.dev/learn/choosing-the-state-structure#avoid-duplication-in-state) -> *"The contents of the selectedItem is the same object as one of the items inside the items list. This means that the information about the item itself is duplicated in two places."* +> 📖 [Choosing the State Structure — Avoid duplication in state](https://react.dev/learn/choosing-the-state-structure#avoid-duplication-in-state) > _"The contents of the selectedItem is the same object as one of the items inside the items list. This means that the information about the item itself is duplicated in two places."_ ```ts // ❌ const [selectedItem, setSelectedItem] = useState(items[0]); @@ -79,8 +74,7 @@ Copying a selected item from a list into state → stale when source updates. St State values that always change together → single setState for atomic updates. -> 📖 [Choosing the State Structure — Group related state](https://react.dev/learn/choosing-the-state-structure#group-related-state) -> *"If some two state variables always change together, it might be a good idea to unify them into a single state variable."* +> 📖 [Choosing the State Structure — Group related state](https://react.dev/learn/choosing-the-state-structure#group-related-state) > _"If some two state variables always change together, it might be a good idea to unify them into a single state variable."_ ```ts // ❌ const [x, setX] = useState(0); const [y, setY] = useState(0); @@ -95,8 +89,7 @@ State values that always change together → single setState for atomic updates. Network, DOM APIs, browser APIs — synchronization only. Not for event handling or data transformation. -> 📖 [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) -> *"Effects are an escape hatch from the React paradigm. They let you 'step outside' of React and synchronize your components with some external system."* +> 📖 [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) > _"Effects are an escape hatch from the React paradigm. They let you 'step outside' of React and synchronize your components with some external system."_ ```ts // ❌ useEffect(() => { if (product.isInCart) showNotification('Added!'); }, [product]); @@ -107,15 +100,13 @@ Network, DOM APIs, browser APIs — synchronization only. Not for event handling One effect sets state → triggers next effect → cascading re-renders + untraceable. Consolidate in event handlers or reducers. -> 📖 [You Might Not Need an Effect — Chains of computations](https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations) -> *"The component (and its children) have to re-render between each set call in the chain."* +> 📖 [You Might Not Need an Effect — Chains of computations](https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations) > _"The component (and its children) have to re-render between each set call in the chain."_ ### U10. Reset State with key Prop `key={id}` forces a clean remount. useEffect reset → stale value visible for one frame. -> 📖 [You Might Not Need an Effect — Resetting all state when a prop changes](https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes) -> *"Instead, you can tell React that each user's profile is conceptually a different profile by giving it an explicit key."* +> 📖 [You Might Not Need an Effect — Resetting all state when a prop changes](https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes) > _"Instead, you can tell React that each user's profile is conceptually a different profile by giving it an explicit key."_ ```ts // ❌ useEffect(() => { setComment(''); }, [userId]); @@ -126,8 +117,7 @@ One effect sets state → triggers next effect → cascading re-renders + untrac Objects/functions declared in the component body get new references every render → effect re-runs every render. -> 📖 [Removing Effect Dependencies — Move dynamic objects and functions inside your Effect](https://react.dev/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect) -> *"Object and function dependencies can make your Effect re-synchronize more often than you need."* +> 📖 [Removing Effect Dependencies — Move dynamic objects and functions inside your Effect](https://react.dev/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect) > _"Object and function dependencies can make your Effect re-synchronize more often than you need."_ ```ts // ❌ const options = { serverUrl, roomId }; @@ -142,15 +132,13 @@ Objects/functions declared in the component body get new references every render Browser APIs, third-party stores → use useSyncExternalStore instead of useState + useEffect. Prevents tearing in concurrent rendering + supports SSR server snapshots. -> 📖 [You Might Not Need an Effect — Subscribing to an external store](https://react.dev/learn/you-might-not-need-an-effect#subscribing-to-an-external-store) -> *"Although it's common to use Effects for this, React has a purpose-built Hook for subscribing to an external store that is preferred instead."* +> 📖 [You Might Not Need an Effect — Subscribing to an external store](https://react.dev/learn/you-might-not-need-an-effect#subscribing-to-an-external-store) > _"Although it's common to use Effects for this, React has a purpose-built Hook for subscribing to an external store that is preferred instead."_ ### U13. Notify Parents from Event Handlers When a child needs to notify a parent about state changes, call the parent's callback in the same event handler — not in useEffect. Prevents cascading re-renders. -> 📖 [You Might Not Need an Effect — Notifying parent components about state changes](https://react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes) -> *"Delete the Effect and instead update the state of both components within the same event handler."* +> 📖 [You Might Not Need an Effect — Notifying parent components about state changes](https://react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes) > _"Delete the Effect and instead update the state of both components within the same event handler."_ ```ts // ❌ useEffect(() => { onChange(isOn); }, [isOn]); @@ -161,15 +149,21 @@ When a child needs to notify a parent about state changes, call the parent's cal fetch/timer/subscription without cleanup → race condition. Fast prop changes cause older responses to overwrite newer ones. -> 📖 [Synchronizing with Effects — Fetching data](https://react.dev/learn/synchronizing-with-effects#fetching-data) -> *"If your Effect fetches something, the cleanup function should either abort the fetch or ignore its result."* +> 📖 [Synchronizing with Effects — Fetching data](https://react.dev/learn/synchronizing-with-effects#fetching-data) > _"If your Effect fetches something, the cleanup function should either abort the fetch or ignore its result."_ ```ts -useEffect(function fetchResults() { - let ignore = false; - fetchAPI(query).then(data => { if (!ignore) setResults(data); }); - return () => { ignore = true; }; -}, [query]); +useEffect( + function fetchResults() { + let ignore = false; + fetchAPI(query).then(data => { + if (!ignore) setResults(data); + }); + return () => { + ignore = true; + }; + }, + [query] +); ``` --- @@ -180,15 +174,13 @@ useEffect(function fetchResults() { Measure with `console.time`. If under 1ms, useMemo overhead exceeds saved computation. -> 📖 [useMemo — How to tell if a calculation is expensive](https://react.dev/reference/react/useMemo#how-to-tell-if-a-calculation-is-expensive) -> *"If the overall logged time adds up to a significant amount (say, 1ms or more), it might make sense to memoize that calculation."* +> 📖 [useMemo — How to tell if a calculation is expensive](https://react.dev/reference/react/useMemo#how-to-tell-if-a-calculation-is-expensive) > _"If the overall logged time adds up to a significant amount (say, 1ms or more), it might make sense to memoize that calculation."_ ### U16. useCallback Only When Passing to memo()-Wrapped Children Stable reference to a non-memo() child has zero re-render prevention effect. -> 📖 [useCallback](https://react.dev/reference/react/useCallback) -> *"You should only rely on useCallback as a performance optimization. If your code doesn't work without it, find the underlying problem and fix it first. Then you may add useCallback back."* +> 📖 [useCallback](https://react.dev/reference/react/useCallback) > _"You should only rely on useCallback as a performance optimization. If your code doesn't work without it, find the underlying problem and fix it first. Then you may add useCallback back."_ --- @@ -199,5 +191,4 @@ Stable reference to a non-memo() child has zero re-render prevention effect. No lifecycle wrappers (`useMount`, `useEffectOnce`). Only purpose-specific hooks (`useWindowSize`, `useOnlineStatus`). Extraction criterion: Does the same state+effect pattern repeat in 2+ components? -> 📖 [Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) -> *"Custom Hooks let you share stateful logic, not state itself."* +> 📖 [Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) > _"Custom Hooks let you share stateful logic, not state itself."_ diff --git a/packages/plugin/README.md b/packages/plugin/README.md index 7588bebe..6b603b6c 100644 --- a/packages/plugin/README.md +++ b/packages/plugin/README.md @@ -40,14 +40,14 @@ Write hooks following design philosophy. Themed guide with code examples. ## Principles Overview -| Category | Count | Examples | -|----------|-------|---------| -| React design | 5 | Declarative interface, lifecycle respect, minimal surfaces, reliability, zero-dependency bias | -| Coding (C1-C14) | 14 | Always return objects, SSR-safe init, no `any`, cleanup | -| State Design (U1-U3, U5-U7) | 6 | Derive don't sync, useRef for non-rendered, discriminated unions | -| Effect Usage (U8-U14) | 7 | Effects for sync only, no chains, key reset, async cleanup | -| Memoization (U15-U16) | 2 | useMemo >= 1ms, useCallback + memo() only | -| Hook Design (U17) | 1 | Extract reusable logic, not lifecycle wrappers | +| Category | Count | Examples | +| --------------------------- | ----- | --------------------------------------------------------------------------------------------- | +| React design | 5 | Declarative interface, lifecycle respect, minimal surfaces, reliability, zero-dependency bias | +| Coding (C1-C14) | 14 | Always return objects, SSR-safe init, no `any`, cleanup | +| State Design (U1-U3, U5-U7) | 6 | Derive don't sync, useRef for non-rendered, discriminated unions | +| Effect Usage (U8-U14) | 7 | Effects for sync only, no chains, key reset, async cleanup | +| Memoization (U15-U16) | 2 | useMemo >= 1ms, useCallback + memo() only | +| Hook Design (U17) | 1 | Extract reusable logic, not lifecycle wrappers | ## Philosophy diff --git a/packages/plugin/skills/react-hook-review/SKILL.md b/packages/plugin/skills/react-hook-review/SKILL.md index 619fecd7..5cf861a5 100644 --- a/packages/plugin/skills/react-hook-review/SKILL.md +++ b/packages/plugin/skills/react-hook-review/SKILL.md @@ -38,7 +38,7 @@ Treat C1, C7, and C14 as opinionated conventions unless the target codebase expl Why: AI doc generation quality + IDE tooltips. 9. **Performance (C10)** — Throttle (16ms) for >30 events/sec, deduplicate unchanged, startTransition for non-urgent. - Only applies to high-frequency event hooks. + Only applies to high-frequency event hooks. 10. **Zero deps (C12)** — No runtime dependencies. peerDependencies only. @@ -96,18 +96,22 @@ Treat C1, C7, and C14 as opinionated conventions unless the target codebase expl ## Output Format ### Great Work + - [What was done well] ### Required Changes + 1. **[C#/U#]** Issue description - Current: `code` - Suggested: `code` - Why: [reason] ### Suggestions + - [Non-blocking improvements] ### Next Steps + 1. Fix required changes 2. Run test suite 3. Commit diff --git a/packages/plugin/skills/react-hook-writing/SKILL.md b/packages/plugin/skills/react-hook-writing/SKILL.md index cda6860e..fe6fc45b 100644 --- a/packages/plugin/skills/react-hook-writing/SKILL.md +++ b/packages/plugin/skills/react-hook-writing/SKILL.md @@ -15,8 +15,13 @@ Treat C1 and C7 as project conventions rather than universal React rules. React Why: Named fields, order-independent, extensible without breaking changes. ```ts -function useDebounce({ value, delay }: { value: T; delay: number }): { value: T } -function useToggle({ initial }: { initial?: boolean }): { value: boolean; toggle: () => void } +function useDebounce({ value, delay }: { value: T; delay: number }): { + value: T; +}; +function useToggle({ initial }: { initial?: boolean }): { + value: boolean; + toggle: () => void; +}; ``` **Parameters (C7):** Object props, not positional. Order-independent + self-documenting. @@ -92,6 +97,7 @@ Async effects need ignore flags or AbortController to prevent race conditions (U ## 6. Performance (C10) Apply only to >30 events/sec (scroll, resize, keyboard): + - **Throttle** at 16ms (60fps) - **Deduplicate**: skip setState when value unchanged - **startTransition**: expensive non-urgent computations diff --git a/packages/plugin/skills/react-hook-writing/references/patterns.md b/packages/plugin/skills/react-hook-writing/references/patterns.md index 5d2f9abb..ea5bede0 100644 --- a/packages/plugin/skills/react-hook-writing/references/patterns.md +++ b/packages/plugin/skills/react-hook-writing/references/patterns.md @@ -72,18 +72,23 @@ import { useState, useEffect, useRef } from 'react'; * fetch(`/api/search?q=${debouncedQuery}`); * }, [debouncedQuery]); */ -export function useDebounce({ value, delay }: { value: T; delay: number }): { value: T } { +export function useDebounce({ value, delay }: { value: T; delay: number }): { + value: T; +} { const [debouncedValue, setDebouncedValue] = useState(value); - useEffect(function scheduleUpdate() { - const timer = setTimeout(function applyUpdate() { - setDebouncedValue(value); - }, delay); + useEffect( + function scheduleUpdate() { + const timer = setTimeout(function applyUpdate() { + setDebouncedValue(value); + }, delay); - return function cancelPendingUpdate() { - clearTimeout(timer); - }; - }, [value, delay]); + return function cancelPendingUpdate() { + clearTimeout(timer); + }; + }, + [value, delay] + ); return { value: debouncedValue }; } @@ -120,38 +125,43 @@ import { useState, useEffect, useRef } from 'react'; * const { matches: isMobile } = useMediaQuery({ query: '(max-width: 768px)' }); * return isMobile ? : ; */ -export function useMediaQuery({ query }: { query: string }): { matches: boolean } { +export function useMediaQuery({ query }: { query: string }): { + matches: boolean; +} { const [matches, setMatches] = useState(false); // C2: Fixed initial value (SSR-safe) const prevMatchesRef = useRef(false); // U3: Non-rendered value - useEffect(function syncMediaQuery() { - if (typeof window === 'undefined') { - return; - } + useEffect( + function syncMediaQuery() { + if (typeof window === 'undefined') { + return; + } - const mediaQueryList = window.matchMedia(query); + const mediaQueryList = window.matchMedia(query); - function handleChange() { - const nextMatches = mediaQueryList.matches; + function handleChange() { + const nextMatches = mediaQueryList.matches; - // C10: Deduplicate — skip if unchanged - if (prevMatchesRef.current === nextMatches) { - return; + // C10: Deduplicate — skip if unchanged + if (prevMatchesRef.current === nextMatches) { + return; + } + prevMatchesRef.current = nextMatches; + setMatches(nextMatches); } - prevMatchesRef.current = nextMatches; - setMatches(nextMatches); - } - // Initial sync - handleChange(); + // Initial sync + handleChange(); - // Subscribe - mediaQueryList.addEventListener('change', handleChange); + // Subscribe + mediaQueryList.addEventListener('change', handleChange); - return function cleanupMediaQuery() { - mediaQueryList.removeEventListener('change', handleChange); - }; - }, [query]); + return function cleanupMediaQuery() { + mediaQueryList.removeEventListener('change', handleChange); + }; + }, + [query] + ); return { matches }; } @@ -183,34 +193,41 @@ function handleChange() { Generic template for hooks that access browser APIs: ```ts -export function useExample({ param }: { param: ParamType }): { value: ReturnType } { +export function useExample({ param }: { param: ParamType }): { + value: ReturnType; +} { const [value, setValue] = useState(FIXED_INITIAL); // C2: SSR-safe - const prevRef = useRef(FIXED_INITIAL); // U3: non-rendered - - useEffect(function syncBrowserValue() { - if (typeof window === 'undefined') { - return; - } + const prevRef = useRef(FIXED_INITIAL); // U3: non-rendered - // Initial sync - const current = getBrowserValue(param); - prevRef.current = current; - setValue(current); + useEffect( + function syncBrowserValue() { + if (typeof window === 'undefined') { + return; + } - // Subscribe to changes - function handleChange() { - const next = getBrowserValue(param); - if (prevRef.current === next) { return; } // C10: dedup - prevRef.current = next; - setValue(next); - } + // Initial sync + const current = getBrowserValue(param); + prevRef.current = current; + setValue(current); + + // Subscribe to changes + function handleChange() { + const next = getBrowserValue(param); + if (prevRef.current === next) { + return; + } // C10: dedup + prevRef.current = next; + setValue(next); + } - window.addEventListener('event', handleChange); + window.addEventListener('event', handleChange); - return function cleanup() { - window.removeEventListener('event', handleChange); - }; - }, [param]); + return function cleanup() { + window.removeEventListener('event', handleChange); + }; + }, + [param] + ); return { value }; } @@ -220,15 +237,15 @@ export function useExample({ param }: { param: ParamType }): { value: ReturnType ## Anti-Pattern Collection -| Anti-Pattern | Principle Violated | Fix | -|---|---|---| -| `useState(window.innerWidth)` | C2 (SSR) | `useState(0)` + useEffect sync | -| Missing cleanup on addEventListener | C3 (Cleanup) | Return removeEventListener | -| `function useData(url: any): any` | C4 (No any) | Use generic `` | -| `export default useHook` | C5 (Named exports) | `export function useHook` | -| `if (count)` where count can be 0 | C6 (Strict booleans) | `if (count != null)` | -| `useEffect(() => { setFullName(...) }, [first, last])` | U1 (Derive) | `const fullName = first + last` | -| `const [color] = useState(colorProp)` | U2 (Mirror props) | `const color = colorProp` | -| `const [id, setId] = useState(null)` for non-rendered | U3 (useRef) | `useRef(null)` | -| chained useEffects setting state | U9 (No chains) | Consolidate in handler | -| `useMemo(() => items.filter(...), [items])` on 20 items | U15 (Measure first) | Plain computation | +| Anti-Pattern | Principle Violated | Fix | +| ------------------------------------------------------- | -------------------- | ------------------------------- | +| `useState(window.innerWidth)` | C2 (SSR) | `useState(0)` + useEffect sync | +| Missing cleanup on addEventListener | C3 (Cleanup) | Return removeEventListener | +| `function useData(url: any): any` | C4 (No any) | Use generic `` | +| `export default useHook` | C5 (Named exports) | `export function useHook` | +| `if (count)` where count can be 0 | C6 (Strict booleans) | `if (count != null)` | +| `useEffect(() => { setFullName(...) }, [first, last])` | U1 (Derive) | `const fullName = first + last` | +| `const [color] = useState(colorProp)` | U2 (Mirror props) | `const color = colorProp` | +| `const [id, setId] = useState(null)` for non-rendered | U3 (useRef) | `useRef(null)` | +| chained useEffects setting state | U9 (No chains) | Consolidate in handler | +| `useMemo(() => items.filter(...), [items])` on 20 items | U15 (Measure first) | Plain computation | From 4683b24c6f64b7c97724aadc9e4a430491048d74 Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Tue, 21 Apr 2026 23:16:19 +0900 Subject: [PATCH 14/14] chore: trigger changeset-bot verification