Verified against Claude Code · 2026-07-14
Extract a reusable hook from tangled component logic
Pulls one piece of stateful logic out of a component into a properly named custom hook with a minimal, typed return contract, without changing runtime behavior.
The prompt
Ready to copy — highlighted parts are example details you can swap.
<role>
You are a senior React engineer extracting a custom hook from an existing component without changing its runtime behavior.
</role>
<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 (...); }
</component>
<extraction_target>
Pull out the logic responsible for: the debounced search input state and the effect that fires the API call and stores the results
</extraction_target>
<rules>
- Name the hook use<Something> based on what it does, not how it's implemented (useDebouncedSearch, not useStateAndEffect).
- The hook's return value must be a small, explicit object or tuple — nothing in the component should need to reach into unrelated state to make sense of it.
- Preserve every dependency array exactly as-is unless you can name the specific stale-closure or missing-dependency bug the current array causes.
- Do not introduce new state to "clean up" the API if the component genuinely needs the old shape — this is an extraction, not a redesign.
- If the logic depends on something specific to this one component (a prop name, a DOM ref only this component has), leave a TODO noting what would need to become a parameter to make the hook truly reusable elsewhere.
</rules>
<output_format>
1. The new hook, in its own code block, with a one-line comment above it stating its contract (inputs → outputs).
2. The component's render body, updated to call the hook instead of containing the extracted logic — nothing else in the component should change.
3. A short note on anything you deliberately left component-specific, per the TODO rule above.
</output_format>Customize the highlighted detailsoptional — the prompt above already works
Why this works
The use<Something>-by-behavior naming rule forces the model to think about 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. Preserving the dependency array unless a specific bug is named stops a common failure mode where an LLM "fixes" a useEffect dependency array during an unrelated extraction and silently changes when the effect fires. The mandatory TODO for component-specific coupling matters because the biggest 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 reader actually reuse the hook instead of copy-pasting it and hoping.
What you get back
function useDebouncedSearch(delay = 300) { // Inputs: none. Outputs: { query, setQuery, results }. const [query, setQuery] = useState(''); const [results, setResults] = useState([]); useEffect(() => { const id = setTimeout(() => { fetch('/api/search?q=' + query).then(r => r.json()).then(setResults); }, delay); return () => clearTimeout(id); }, [query, delay]); return { query, setQuery, results }; } function ProductSearch() { const { query, setQuery, results } = useDebouncedSearch(); return (/* unchanged JSX */); } Note: no component-specific coupling found — this hook is reusable as-is.
Verified against
Claude Code Sonnet 4.6 · 2026-07-14
Cursor 2.1 · 2026-07-20
Changelog
- 2026-07-14 — 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

