Skip to main content

react-hooks

React hooks and Context conventions for this project. Covers custom hook extraction rules, Context splitting (state vs actions), dependency management with biome useExhaustiveDependencies, and ref/callback patterns. Use when writing or reviewing custom hooks, Context providers, or component re-render optimization.

설치로 이동

소스 정보

저장소
recca0120/code-quest
최근 소스 활동
2026년 4월 21일 19:48
감지된 SKILL.md 언어
영어
스타
11
포크
2

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
2 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
react-hooks
description
React hooks and Context conventions for this project. Covers custom hook extraction rules, Context splitting (state vs actions), dependency management with biome useExhaustiveDependencies, and ref/callback patterns. Use when writing or reviewing custom hooks, Context providers, or component re-render optimization.
# React Hooks Best Practices ## Overview Guidelines for writing custom hooks, choosing the right hook, and avoiding common pitfalls. Optimized for React 19 + biome `useExhaustiveDependencies`. ## When to Use - Creating or modifying custom hooks - Deciding whether to use `useEffect`, `useMemo`, `useCallback` - Debugging stale closures, infinite loops, or unnecessary re-renders - Fixing biome/eslint exhaustive-deps warnings --- ## Custom Hook Design ### When to Extract a Custom Hook **Rule: only extract when there is actual duplication.** Two or more components must share the same stateful logic before a hook is worth creating. A single consumer → keep the logic inline. Extracting a hook "for organization" with only one caller is over-engineering. ```ts // Good: reusable across components function useDebounce<T>(value: T, delay: number): T { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(id); }, [value, delay]); return debounced; } ``` ### Naming & Return Values - Always prefix with `use` - Return **object** for 3+ values, **tuple** for 1-2 values - Return stable references: actions via `useCallback`, derived data via `useMemo` ```ts // Tuple for simple hooks function useToggle(initial = false): [boolean, () => void] { ... } // Object for complex hooks function useChat(socket: TypedSocket) { return { sendMessage, abort, createSession }; } ``` ### Composition Over Complexity Build complex hooks by composing simpler ones. Each hook should have a single responsibility. ```ts // Good: composed function useChatPanel(socket: TypedSocket) { const chat = useChat(socket); const store = useChannel((s) => ({ messages: s.messages, status: s.status })); return { ...chat, ...store }; } ``` --- ## You Might Not Need an Effect Most `useEffect` calls fall into two categories — **only one needs useEffect**. ### Don't Need useEffect | Pattern | Instead | |---------|---------| | Transform data for render | Compute during render (`const x = derive(props)`) | | Cache expensive computation | `useMemo(() => compute(deps), [deps])` | | Reset state when prop changes | Use `key` prop on component | | Respond to user event | Handle in event handler directly | | POST on form submit | Handle in `onSubmit`, not in effect | | Initialize app once | Module-level code or `useRef` guard | ```ts // Bad: useEffect to filter const [filtered, setFiltered] = useState(items); useEffect(() => { setFiltered(items.filter(predicate)); }, [items]); // Good: derive during render const filtered = useMemo(() => items.filter(predicate), [items]); ``` ### Do Need useEffect - Synchronize with **external systems** (DOM APIs, WebSocket, timers, third-party libs) - Subscribe/unsubscribe to events (always return cleanup) ```ts // Good: external system sync useEffect(() => { socket.on('chat:event', onEvent); return () => { socket.off('chat:event', onEvent); }; }, [socket]); ``` --- ## Context Best Practices — Split State and Actions ### Two-Context Pattern (Required for Complex Providers) When a context has both state (changes frequently) and actions (stable functions), **split them into separate contexts**. This prevents components that only call actions from re-rendering when state changes. ```tsx // ✅ Good: separate contexts const StateContext = createContext<State | null>(null); const ActionsContext = createContext<Actions | null>(null); function Provider({ children }: { children: ReactNode }) { const [state, dispatch] = useReducer(reducer, initialState); // Actions are stable — only depend on dispatch (which is stable) const actions = useMemo(() => ({ sendMessage: (msg: string) => dispatch({ type: 'SEND', msg }), abort: () => dispatch({ type: 'ABORT' }), }), [dispatch]); return ( <StateContext.Provider value={state}> <ActionsContext.Provider value={actions}> {children} </ActionsContext.Provider> </StateContext.Provider> ); } // Consumers pick what they need function SendButton() { const { sendMessage } = useActions(); // stable — no re-render on state change return <button onClick={() => sendMessage('hi')}>Send</button>; } function MessageList() { const { messages } = useState(); // re-renders when messages change return <ul>{messages.map(m => <li key={m.id}>{m.content}</li>)}</ul>; } ``` ### When NOT to Split - Simple contexts with <5 fields and few consumers - Contexts where every consumer needs both state and actions - Prototyping / early development (split when performance matters) ### UI state vs Data state UI-only state (toggle 開關、dialog open、hover) 獨立放 UI context,不混進 data context。Data context 改動觸發的 re-render 不該影響「我剛打開一個 dropdown」這種純 UI 狀態。 ### Component 職責 - Socket / RPC 呼叫走 Context action(handler),component 只呼叫 action — 不在 component 直接 `socket.emit(...)` - Component 讀多個 context 只是為了把 props 橋接給子 component → 讓子 component 自己讀 context,父不必中介 ### Bind class methods before passing through Context Class methods lose `this` binding when destructured. If a handler is a class instance, wrap its methods with `.bind()` or arrow functions in `useMemo` before putting them on the context value: ```tsx // ❌ Bad: class instance — methods lose `this` on destructure <Context.Provider value={handler}> // ✅ Good: bind once in useMemo const actions = useMemo(() => ({ send: handler.send.bind(handler), abort: handler.abort.bind(handler), }), [handler]); ``` ### `useReducer` dispatch is Stable `useReducer` returns a stable `dispatch` — React guarantees it never changes. No need for `useCallback` or `useMemo` around dispatch. ```tsx const [state, dispatch] = useReducer(reducer, init); // dispatch is stable — safe to use in deps without causing re-creation ``` ### Context Value Stability Provider value 可以直接寫 inline — Compiler 會 memo。詳見 `react-compiler` skill。 ```tsx <Ctx.Provider value={{ count, increment }}> {/* Compiler auto-memo */} ``` --- ## useCallback / useMemo / React.memo **This project uses React Compiler.** 大部分情境 Compiler 會自動 memo,不用手動加。但有 3 個架構邊界必須手動處理(component `React.memo`、prop capture 意圖、高頻 setState commit)— 完整規則見 **`react-compiler` skill**。 --- ## Refs、dependency arrays 與 biome useExhaustiveDependencies biome 的 `useExhaustiveDependencies` 規則、component-scoped function 的 false positive 修法、`useChannel.getState()` 在 callback 裡的用法見 `references/biome-deps-and-refs.md`。快速結論:useRef 值、useState setter、useReducer dispatch、useTransition startTransition 這四類是 stable,其他都視為 dep。 ## Testing Custom Hooks **Always use `@testing-library/react` as the first choice** — either `renderHook` for isolated hook testing, or `render` + `screen` when testing the hook through a component. Do not call hook functions directly, do not mock React internals. Use `renderHook` from `@testing-library/react`: ```ts import { renderHook, act } from '@testing-library/react'; it('toggles value', () => { const { result } = renderHook(() => useToggle(false)); expect(result.current[0]).toBe(false); act(() => { result.current[1](); }); expect(result.current[0]).toBe(true); }); ``` For hooks with external dependencies (socket, API), inject via parameters — avoid `vi.mock` when possible. --- ## Quick Reference | Rule | Rationale | |------|-----------| | Prefix with `use` | React's hook detection relies on naming | | No conditional hooks | Hooks must be called in same order every render | | Cleanup in useEffect | Prevent memory leaks, stale subscriptions | | Derive during render | Avoids unnecessary state + effect cycles | | Module-level helpers | Avoids biome exhaustive-deps false positives | | `useRef` for mutable flags | Stable identity, no re-renders, biome-safe | | `getState()` for store reads in callbacks | No subscription, always fresh, no deps needed | ## Common Mistakes | Mistake | Fix | |---------|-----| | `useEffect` + `setState` for derived data | Compute during render or `useMemo` | | Missing cleanup in useEffect | Always return cleanup for subscriptions/timers | | Component-scoped helper in dep array | Move to module level or inline | | Wrapping every function in `useCallback` | Only when passed to memoized children or in deps | | `ref.current` in dep array | Refs are mutable — biome ignores them; use event-based updates | | Effect runs on every render | Check dependency array; empty `[]` for mount-only | ## 相關 skill - Context 測試 / socket 互動 → `fake-summoner-client` - Hook / component 測試 RTL 慣例 → `frontend-testing` / `testing-best-practices` - Store pattern(zustand 當 state 替代)→ `zustand-state` - Tailwind class + axis 影響 render → `tailwind-v4`
GitHub에서 보기