When I first started using Claude Code on React work, I noticed it writing code I'd have corrected in review.
Two anti-patterns kept appearing. The first was useCallback wrapped around every event handler, often with an empty dependency array. The second was setState inside a useEffect to derive a value from props.
Here is roughly what it looked like:
const Panel = ({items, selectedId, onClose}: Props) => {
const [item, setItem] = useState<Item | null>(null);
useEffect(() => {
const next = items.find((i) => i.id === selectedId);
setItem(next ?? null);
}, [items, selectedId]);
const handleClickClose = useCallback(() => {
onClose();
}, []);
// ...
};useEffect is doing work that should happen during render. useCallback is wrapping a handler with no reason to be memoized, with an empty dependency array that freezes the handler to the first render's onClose. Neither breaks the build. Both would ship if no one was checking.
Working code !== correct code. There's the performance cost and the technical debt, but the harder cost is precedent. An anti-pattern that ships becomes part of the codebase, gets copied into the next component, and turns into "how we do it here." The anti-pattern becomes a convention.
This was always true with humans. What changes with AI is the speed of reinforcement. By the time you notice the pattern, it's already the dominant shape.
The anti-patterns
I'd seen these common anti-patterns in production React for years. They're the kind of thing senior engineers catch in code review and tag with a link to the React docs. Now Claude was producing them by default.
useEffect for derived state is one of the most documented anti-patterns in the React ecosystem. The matching item is derivable from items and selectedId. There is no external system to synchronize with. The Effect adds a render cycle, opens the door to infinite render loops, and obscures intent.
useCallback wrapped around every handler is the other one. The hook only does anything useful when the function is passed to a memo-wrapped child or used as a dependency in another hook. Wrapping everything defensively allocates the dependency array on every render, hides stale-closure bugs behind empty []s, and forces every reviewer to figure out which case the wrap is solving. In the snippet above, it's solving none.
Why Claude does this
Not React tutorials. Tutorials teach to avoid this.
AI's training corpus contains a lot of React written by engineers who didn't choose to be writing it, or weren't familiar with current React conventions. Sometimes backend engineers are required to write frontend code on a deadline, code that "technically worked" and shipped as-is.
Claude reproduces the average of the corpus. The anti-patterns are in the average.
The question isn't "how do I get a smarter model." Claude can write React well. The question is how to get the standard in front of the model so it stops defaulting to the average.
What I did
I took the official React docs and You Might Not Need an Effect and shaped them into a skill the project auto-loads on any React work. Now when Claude considers reaching for useEffect, it has to answer six questions first:
- Can I calculate this during render?
- Does this respond to a user action?
- Am I syncing state to state?
- Am I notifying a parent of a state change?
- Do I need to reset child state when a prop changes?
- Am I synchronizing with an external system?
If the answer to any of the first five is yes, the Effect is the wrong tool. The same skill names the only three cases where useCallback belongs:
- Passed to a
memo-wrapped child. - Used as a dependency of another hook.
- Passed to a child that uses it in a hook dependency array.
Anywhere else, the function stays a plain inline function.
The result
The anti-patterns stopped appearing.
Not because the model changed. The standard was now in context. Claude had access to the same React-team guidance a senior engineer would have given, and followed it.
What this became
GAIA already had static checking built in to help human engineers avoid the most common issues. The point was to free senior engineers reviewing PRs to focus on real anti-patterns, because the rest of the code was already correct.
Applying that same philosophy to AI was what inspired me to expand GAIA with AI guardrails. AI is still an engineer. It walks into a new codebase without your team's context, the same way any new engineer would. So I run the same loop. I review the work, teach the patterns, encode the standard.
Learning never stops, not even for experts. The difference is AI loses the context at the end of every session, so the standard has to live in the project, in code, where Claude reads it without being asked. The hooks skill was the first piece. Since then I've added skills for TypeScript, for Tailwind, for testing, and a Claude Code hook that flags hardcoded JSX strings. Same principle every time, applied to a different kind of engineer.
That principle is portable. Anything you'd want a senior reviewer to flag for a human engineer, you can encode in code that Claude follows automatically.
GAIA is open source at github.com/gaia-react/gaia.