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.

Zur Installation springen

Quellinformationen

Repository
recca0120/code-quest
Letzte Quellaktivität
21. April 2026 um 19:48
Erkannte Sprache von SKILL.md
Englisch
Sterne
11
Forks
2

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
2 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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`
Auf GitHub ansehen