react-hook
React Hook Patterns
Overview
Core principles:
- Callbacks passed to hooks must be wrapped in
useCallback - One hook per file, organized by feature domain
- Only abstract when logic is reused or complex
Custom Hook Rules
| Rule | Why |
|---|---|
Must start with use |
React's hook detection |
| One hook per file | Maintainability |
| Never call conditionally | Breaks hook order |
| Never return side effects | Unpredictable behavior |
| Type inputs and outputs | Clarity and safety |
| Test in isolation | Reliability |
On memoization: Only use useMemo/useCallback when logic is computationally heavy. Otherwise they degrade readability without meaningful benefit. Exception: callbacks passed TO hooks (see stability section below).
When to Use
- Passing a callback to a custom hook
- Fixing
react-hooks/exhaustive-depsESLint warnings - Debugging "why is this re-rendering every keystroke?"
- Seeing errors like
addRange(): The given range isn't in document
The Problem
Inline callback → Hook depends on it → Hook's output in useMemo → Cascade of re-renders
When you fix an ESLint exhaustive-deps warning by adding a dependency, check if that dependency is STABLE. If not, you've created a re-render loop.
Core Pattern
// ❌ BAD - inline function recreated every render
const { handler } = useCustomHook({
onComplete: (result) => doSomething(result)
});
// ✅ GOOD - stable reference
const onComplete = useCallback((result) => doSomething(result), []);
const { handler } = useCustomHook({ onComplete });
Quick Reference
| Situation | Action |
|---|---|
| Passing callback to hook | Wrap in useCallback |
| ESLint says add dependency | Check if dependency is stable first |
| Hook output changes every render | Trace dependency chain backwards |
| Component re-renders on every keystroke | Check for inline callbacks in hook calls |
Red Flags - STOP and Check
If you're about to:
- Pass an inline arrow function to a custom hook
- Add a callback to
useMemo/useCallbackdeps without checking stability - "Fix" exhaustive-deps by just adding the missing dep
STOP. Trace the dependency chain. Is everything stable?
Common Mistakes
| Mistake | Fix |
|---|---|
| "ESLint said add it, so I did" | Check if the dep is stable BEFORE adding |
| "It's just a small callback" | Size doesn't matter, stability does |
| "The hook should handle this" | Caller is responsible for stable refs |
Dependency Chain Debugging
When something re-renders unexpectedly:
- Find the
useMemo/useCallbackthat's recreating - Check each dependency - which one changed?
- Trace that dependency back - why did IT change?
- Keep tracing until you find the unstable root
- Wrap the root in
useCallbackwith stable deps
Real-World Impact
The bug: Quill editor threw addRange(): The given range isn't in document on every keystroke.
Root cause:
- ESLint fix changed
useMemodeps from[toast]to[imageHandler] imageHandlerdepended oninsertImageviauseCallbackinsertImagewas inline (new function every render)- Chain:
insertImage↻imageHandler↻modules↻ ReactQuill re-init
Fix: Wrap insertImage in useCallback with [] deps.
Time saved by knowing pattern: 2+ hours of debugging → 5 minutes.
More from bumgeunsong/daily-writing-friends
firebase-functions
Use when creating or modifying Firebase Cloud Functions in /functions directory. Enforces function structure and error handling patterns.
43pr-stacking
PR stacking workflow for breaking large features into smaller, dependent PRs. Use when planning multi-step features, creating dependent branches, or rebasing stacked changes.
29commit
Use when creating git commits in this project
28refactoring
Use when user explicitly asks to refactor code, or when test coverage is requested for untested code with side effects. Enforces Functional Core Imperative Shell pattern extraction before any changes.
28api-layer
Use when creating or modifying API functions in */api/ directories. Enforces Firestore patterns and data fetching conventions.
28code-style
Use when writing or modifying any code. Enforces naming conventions, function design, and code clarity principles.
28