跪拜 Guibai
← All articles
Frontend · JavaScript · React.js

6 AI-Generated React Bad Smells to Catch Before You Merge

By kyriewen ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

AI coding tools produce React at high volume, and the bugs they introduce are not syntax errors — they are silent runtime problems that pass manual testing of a single flow. A team merging AI-generated PRs without a checklist will accumulate extra render cycles, stale form data, and broken memoization that degrade UX and inflate the performance budget with no visible cause.

Summary

AI-generated React code carries six recurring bad smells that survive code review because the syntax is flawless. The most common is stuffing derived calculations into useEffect, which adds an unnecessary render cycle and risks state drift. Copying props into useState creates a subtle bug where the form silently shows stale data after a parent re-render. Fetch calls inside useEffect without an AbortController produce race conditions that flip the UI back to old data when responses arrive out of order.

Using array index as a list key breaks input focus, internal component state, and animations whenever the list reorders. Pairing React.memo with inline objects or arrow functions makes the memoization useless while adding shallow-comparison overhead. The inverse problem — wrapping every value in useMemo and every callback in useCallback — often makes code slower than plain computation because the hook overhead exceeds the cost of the work being memoized.

A six-question checklist catches all of these in under a minute: check whether useEffect logic can live in the render phase, whether useState ever reads from props, whether async effects have cleanup, whether list keys are stable ids, whether memo components receive stable references, and whether each useMemo or useCallback actually prevents a real re-computation.

Takeaways
Derived data should be computed directly during render, not inside useEffect with setState — the effect pattern adds an extra render cycle and risks state falling out of sync with props.
useState(props.xxx) reads the prop only on mount; when the parent passes a new value, the state stays stale. Either lift state up or force remount with a key.
Every async call inside useEffect needs a cleanup function — typically an AbortController — or a data-fetching library like TanStack Query that handles race conditions automatically.
Using array index as a list key breaks input focus, mismatches component state, and corrupts animations when items are inserted, deleted, or reordered.
React.memo is defeated by inline objects, arrays, or arrow functions passed as props because they create new references on every render, making the shallow comparison always fail.
useMemo and useCallback carry their own cost — a dependency-array comparison and a memory slot — and are only worth it when the computation is expensive or the value is passed to a memoized child.
Before merging, run a six-point checklist: can useEffect logic move to render? Is useState reading from props? Does async code have cleanup? Are list keys stable? Do memo components get stable references? Would removing useMemo/useCallback actually break anything?
Conclusions

The core problem is not that AI misunderstands React syntax — it's that React best practices are decisions about an entire component tree, and AI only sees the snippet it was given. The same model that writes correct JavaScript will confidently produce a useEffect that looks idiomatic but adds a render cycle because it doesn't know the parent already handles the data.

AI's tendency to overuse hooks — stuffing things into useEffect, wrapping everything in useMemo — mirrors what a junior developer does after reading the docs but before experiencing production. The model has absorbed the API surface without the cost model.

The six patterns form two opposing failure modes: AI adds state and effects where none are needed (patterns 1 and 2), and it adds memoization that either doesn't work (pattern 5) or works but costs more than it saves (pattern 6). Both come from the same root cause — the model cannot see the runtime tradeoffs.

Race conditions in useEffect fetches are a known footgun that every data-fetching library solves, yet AI-generated code almost never uses those libraries unless explicitly prompted. This suggests the training data contains more raw fetch examples than library-adoption examples.

Concepts & terms
Derived state
Values that can be computed directly from existing props or state during render, without storing them in useState or useEffect. Computing them inline avoids extra render cycles and keeps the UI consistent with the source data.
Race condition in useEffect fetch
When multiple async requests are fired in sequence (e.g., as a user clicks through items), responses may return out of order. Without an AbortController or stale flag, a late-arriving response can overwrite newer data, causing the UI to flash back to stale content.
React.memo shallow comparison
React.memo prevents re-renders by comparing previous and current props with Object.is. Inline objects, arrays, and arrow functions create new references on every render, so the comparison always fails and memoization is bypassed — adding overhead with no benefit.
Stable list key
A unique identifier tied to the data item itself (like a database id), not its position in the array. React uses keys to match elements across renders; index-based keys cause state corruption and focus loss when the list order changes.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗