Skip to main content
ai tools

Hooks Are Where AI Coding Gets Serious

AI can scaffold a component in 30 seconds. Hooks with complex timing, cleanup, and dependency arrays are where it earns its keep — or reveals its limits.


6 min read

Ask Claude Code to build a useDebounce hook and you get clean, typed, working code in seconds. Ask it to write a hook that synchronizes an external subscription, manages cleanup across re-renders, and avoids stale closures in its callback — and you start to see where the model's understanding of React's execution model gets shaky. That gap is worth knowing. AI coding tools are genuinely useful for hooks work, but the failure modes are specific and predictable. This is a field report on both.


Where AI Gets Hooks Right

The shape of a custom hook is something AI generates well. TypeScript generics, the return tuple, the initial state type — all of that comes out correctly and fast.

function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue] as const;
}

Claude generates this correctly without prompting. The lazy initializer pattern in useState, the as const return, the generic constraint — it gets these right because they're well-represented in training data and there's a clear, documented happy path.

The same is true for useMediaQuery, useIntersectionObserver, useClickOutside. These are well-defined patterns. The AI has seen thousands of implementations and produces a reasonable synthesis.


Where It Breaks Down

The Dependency Array Problem

The dependency array is where AI-generated hooks most reliably go wrong. The model produces code that looks correct but either over-specifies or under-specifies dependencies:

// AI-generated version — missing stable references in deps
useEffect(() => {
  const subscription = externalStore.subscribe(callback);
  return () => subscription.unsubscribe();
}, []); // callback is stale after the first render

The model produces the empty array because it's seen that pattern in examples where the subscription is meant to run once. It doesn't reason about whether callback is stable. It doesn't know if externalStore came from props or a ref. The code looks plausible and passes a visual review.

The correct version wraps callback in a ref to get a stable function identity, or uses useCallback with its own dependency array. Neither of those solutions comes out of the AI unprompted unless you specifically tell it about the stale closure problem.

useEffect Cleanup

Cleanup functions are another consistent weak point. AI generates the return function, but it frequently generates cleanup that doesn't match what was set up:

// AI-generated — cleanup target doesn't match setup
useEffect(() => {
  document.addEventListener("keydown", handleKeyDown);
  window.addEventListener("resize", handleResize);

  return () => {
    document.removeEventListener("keydown", handleKeyDown); // misses resize
  };
}, [handleKeyDown, handleResize]);

This is a subtle bug. The resize listener leaks. The AI generated the cleanup pattern but didn't audit that every listener added was also removed. It filled in the template without verifying the symmetry.

ref vs state Decisions

When a value needs to be mutable without triggering re-renders, the right call is useRef. AI tools consistently reach for useState instead, because useState is more prominent in training data. You end up with re-renders that didn't need to happen, or — worse — effects that loop because a ref would have been stable where state isn't.


A Practical Workflow

The pattern that works: use AI to generate the hook skeleton, review the dependency array and cleanup manually every time, and write prompts that pre-empt the common failures.

Prompts that produce better results:

  • Be explicit about stability: "The callback parameter will change on every render. Use a ref to hold the latest version and reference it in the effect."
  • Specify cleanup requirements: "This hook adds event listeners. Make sure every listener added in the effect has a corresponding removal in the cleanup function."
  • State the re-render constraint: "This value needs to be mutable but should not cause re-renders. Use useRef, not useState."
  • Name the synchronization goal: "This hook needs to stay in sync with an external subscription that can emit values after unmount. Handle the unmounted case."

When you front-load this context, the output quality improves significantly. The AI isn't reasoning about your application — it's pattern-matching against better examples when you describe the problem in those terms.


The Honest Limitation

The deeper issue is that AI tools don't have a runtime model of React. They don't know that effects run after paint, that cleanup runs before the next effect fires, or that the dependency array is a contract with the reconciler. They know these facts in the same way they know that Paris is the capital of France — as text, not as understanding.

This means the AI can tell you about useEffect cleanup without being able to apply that knowledge reliably in novel combinations. When the hook is simple and the pattern is common, it's fine. When you're composing multiple effects that need to coordinate, or when cleanup order matters, or when you're working around a browser API that has its own async lifecycle — that's where you need to be in the driver's seat.

Claude Code is good at generating the hook skeleton, getting the TypeScript types right on the first pass, and filling in the happy path. It saves meaningful time on that work. The trade is that you have to review the output differently than you'd review human-written code — specifically looking for the failure modes above, not just reading for overall shape.

The developers I've watched get the most out of AI hooks work treat it as pair programming where they're the senior: the AI generates a draft, they check the dependency array like a linter would, they audit the cleanup symmetry, they confirm ref vs state decisions. The AI handles the boilerplate and typing ceremony; the developer handles the concurrency reasoning.

That division of labor is honest about what the tool is actually doing. It's also faster than writing hooks from scratch for the 80% of cases that are genuinely well-patterned.


What to Audit Every Time

Before you ship any AI-generated hook, check these specifically:

  1. Every value from the outer scope that's read inside the effect is in the dependency array — unless it's a ref, a setState function, or a value you've intentionally excluded with a comment explaining why.
  2. Every listener, subscription, timer, or observer added in the effect body has a corresponding removal in the return function.
  3. Any callback passed as a parameter is either wrapped in useCallback by the caller or stored in a ref inside the hook.
  4. If the hook can be used after unmount (WebSocket, fetch, observable), there's a mounted flag or AbortController in the cleanup.

Run eslint-plugin-react-hooks and treat its warnings as errors. It catches the dependency array issues mechanically. It won't catch the cleanup symmetry problems or the ref vs state decisions, but it removes one failure mode from the manual review list.

The AI is a fast first draft. The cleanup reasoning is still yours.