Verified against Claude Code · 2026-07-28
Extract a reusable hook without changing what the component does
Pulls one piece of stateful logic out of a component into a properly named, typed custom hook with a minimal return contract, while treating any behavior change as a bug, not a byproduct.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are a senior React engineer extracting a single piece of stateful logic out of an existing component into a custom hook. This is a mechanical extraction, not a redesign — the component's rendered output and runtime behavior must be identical before and after the change.
COMPONENT
function ProductSearch() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); useEffect(() => { const id = setTimeout(() => fetch("/api/search?q=" + query).then(r => r.json()).then(setResults), 300); return () => clearTimeout(id); }, [query]); return (/* JSX */); }
LOGIC TO EXTRACT
The debounced search input state and the effect that fires the API call and stores the results.
WHY THIS NEEDS TO BE A HOOK
We just added a second component (TagSearch) that needs the identical debounced-fetch behavior, and copy-pasting the effect a second time is how the two will drift out of sync.
TARGET LANGUAGE
TypeScript, strict mode enabled
HOOK NAMING AND CONTRACT
Name the hook use<Something> based on what it does for the caller, never how it happens to be implemented internally — useDebouncedSearch, not useStateAndEffect. The hook must return a small, explicit object or tuple with named fields; nothing in the calling component should have to reach into an unrelated piece of returned state to make sense of what came back. If the hook needs configuration — a delay, an initial value, a callback — make those explicit parameters with sane defaults, not a hidden module-level constant or a context read the caller can't see or override.
DEPENDENCY-ARRAY DISCIPLINE
Preserve every effect's dependency array exactly as it is today unless you can name the specific stale-closure or missing-dependency bug the current array causes. Extraction is not license to "fix" timing nobody asked you to fix — a dependency array that changes when an effect fires is a behavior change wearing a refactor's clothes.
REUSABILITY HONESTY
If the logic depends on something specific to this one component — a particular prop name, a DOM ref only this component happens to have, a string hardcoded for this one screen — do not silently generalize it into a fake-generic parameter, and do not silently leave it hardcoded either. Leave an explicit TODO comment naming exactly what would need to become a parameter before another component could genuinely reuse this hook.
TYPE SAFETY
If the target language is TypeScript, type the hook's parameters and return value explicitly. No implicit any on the returned object, and no widening a specific union the original code relied on (a status of 'idle' | 'loading' | 'error' | 'success') into a bare string.
OUTPUT FORMAT
1. The new hook, in its own code block, with a one-line comment directly above its signature stating its contract (inputs to outputs).
2. The component's render body, updated to call the hook instead of containing the extracted logic — nothing else about the component should change, including formatting you didn't need to touch.
3. A short note listing anything deliberately left component-specific per the TODO rule, or an explicit statement that nothing was left coupled if that's genuinely true.
4. Any judgment call you had to make where these rules didn't fully cover the case, and why you made it.Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
The use<Something>-by-behavior naming rule forces the model to design the hook's contract before its implementation, which is the actual difference between a genuinely reusable hook and a chunk of code that was just moved to a new file with a use prefix stapled on. Preserving the dependency array unless a specific bug is named stops a common and specific failure mode where an LLM quietly "fixes" a useEffect dependency array during an unrelated extraction, changing when the effect fires without being asked to — a change that passes a quick visual diff review because the extraction itself looks clean. The mandatory TODO for component-specific coupling matters because the most common lie in hook extraction is pretending something is generic when it still reads a prop only the original component has; naming that explicitly is what lets a future engineer actually reuse the hook on a second component instead of copy-pasting it, hitting a runtime error, and reverse-engineering what was actually hidden inside. The explicit TypeScript instruction closes a specific and frequent gap: models extracting a hook will often return an object typed as Record<string, unknown> or let the return type get inferred as a bare string, silently discarding a narrower union type (like a request status) the original inline code was implicitly relying on through control flow, which downstream consumers of the hook then can't rely on either. There is also a testability payoff that is easy to miss and expensive to lose: a hook with a small, explicit, named contract can be exercised directly with @testing-library/react's renderHook, asserting on returned values and calling returned setters or handlers without mounting the whole component tree the logic used to live inside, whereas the identical logic buried in a component's body can only be reached by rendering that entire component and simulating a user interaction just to hit a code path that has nothing to do with rendering at all. It also pays off in review: because the hook's inputs and outputs are now named and typed instead of implicit in a block of component logic, a reviewer looking at a future diff to just the hook can understand what changed without re-reading the whole component around it, which is the actual maintenance benefit extraction is supposed to buy and frequently does not when the "extraction" is really just a copy-paste with a use prefix added on top.
What you get back
function useDebouncedSearch(delayMs = 300) { // Inputs: delayMs (optional). Outputs: { query, setQuery, results }. const [query, setQuery] = useState(''); const [results, setResults] = useState<SearchResult[]>([]); useEffect(() => { const id = setTimeout(() => { fetch('/api/search?q=' + query).then(r => r.json()).then(setResults); }, delayMs); return () => clearTimeout(id); }, [query, delayMs]); return { query, setQuery, results }; } function ProductSearch() { const { query, setQuery, results } = useDebouncedSearch(); return (/* unchanged JSX */); } Note: no component-specific coupling found — the endpoint string is the only hardcoded piece, left as a TODO parameter (endpoint: string) since TagSearch will need a different URL.
Verified against
Claude Code Sonnet 4.6 · 2026-07-28
Cursor Cursor 2.1 · 2026-08-01
Changelog
- 2026-07-28 — Initial publish, verified against Claude Code (Sonnet 4.6) and Cursor 2.1.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
