From 5abf645bcd4e60edb5a58744f07984c4429dcc39 Mon Sep 17 00:00:00 2001 From: kimyouknow Date: Fri, 3 Apr 2026 16:40:01 +0900 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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."* ---