The dangerous thing about AI-generated front-end code isn't that it's bad. It's that it's usually well-formatted, which reviewers subconsciously read as a proxy for correct. Consistent indentation, sensible variable names, and no obvious typos make a diff feel trustworthy even when the logic underneath has real problems.
This is a checklist built from the failure modes that show up repeatedly in AI-assisted front-end PRs—not hypothetical risks, but the ones that actually reach production if nobody's looking for them.
1. State ownership
AI assistants are good at making a component work in isolation and less reliable at reasoning about where state should live in a larger tree.
- Is any state duplicated between a parent and child that could drift out of sync?
- Could this state be derived from existing state/props instead of being its own
useState? - If this is global state (context, store), does it need to be—or was local state promoted "just in case"?
// Red flag: two sources of truth that must be kept in sync manually const [items, setItems] = useState(initialItems); const [itemCount, setItemCount] = useState(initialItems.length); // itemCount will silently go stale the moment items changes elsewhere // Fix: derive it const itemCount = items.length;
2. Effects and race conditions
This is the single most common category of AI-introduced bug in front-end code.
- Does every
fetchinside auseEffecthandle the component unmounting before the response returns? - If a dependency changes rapidly (e.g. a search input), does the effect cancel or ignore stale in-flight requests?
- Is the dependency array actually correct, or did the assistant add
// eslint-disable-next-lineto silence a real warning?
// Red flag: no cleanup, no abort — a fast typer triggers a race condition useEffect(() => { fetch(`/api/search?q=${query}`) .then((r) => r.json()) .then(setResults); }, [query]); // Fix: abort stale requests useEffect(() => { const controller = new AbortController(); fetch(`/api/search?q=${query}`, { signal: controller.signal }) .then((r) => r.json()) .then(setResults) .catch((err) => { if (err.name !== "AbortError") throw err; }); return () => controller.abort(); }, [query]);
3. Accessibility, specifically
AI assistants often add ARIA attributes that look correct but don't cover real keyboard and screen-reader flows, because training data skews toward code that was never actually tested with assistive tech.
- Can every interactive element be reached and activated with the keyboard alone (Tab, Enter, Space, Escape where relevant)?
- Do custom components (dropdowns, modals, tabs) manage focus correctly on open/close?
- Are
aria-*attributes paired with actual behavior, not decorative? (aria-expandedon a button that doesn't track open state is worse than no attribute at all.) - Does an image, icon-only button, or form input have a real accessible name?
4. Scope of the diff
- Did the assistant touch files or functions unrelated to the stated task?
- Were any existing patterns (naming conventions, folder structure, shared utilities) bypassed in favor of a new one-off implementation?
- Is there a new dependency that duplicates something already in the project?
A generated diff that "helpfully" reformats an unrelated file, or reinvents a formatCurrency helper that already exists in lib/utils.ts, is a sign the assistant didn't have—or wasn't given—enough codebase context.
5. Error and empty states
AI-generated components reliably nail the happy path and reliably under-specify everything else unless explicitly prompted for it.
- What renders while data is loading?
- What renders if the fetch fails?
- What renders if the data comes back empty (zero results, empty array,
null)? - Are error messages actually useful, or a generic "Something went wrong"?
6. Performance under real data
Code that looks fine with 5 mock items can fall over with 5,000 real ones.
- Is there an unmemoized expensive computation running on every render?
- Is a list rendering without keys, or with array-index keys where items can reorder?
- Is a large list rendered in full when virtualization would be appropriate?
// Red flag: index keys on a reorderable list cause state to attach to the wrong row {items.map((item, i) => <Row key={i} item={item} />)} // Fix: use a stable identifier {items.map((item) => <Row key={item.id} item={item} />)}
The reviewer's actual job now
None of these checks are new—good reviewers have always looked for this. What's changed is the volume of code that needs this level of scrutiny, because generation is nearly free and teams are shipping more of it per day than before.
The practical adjustment: treat "AI wrote the first draft" as a note in the PR description that raises your attention, not lowers it. A clean-looking diff from an assistant deserves the same skepticism as a clean-looking diff from a developer you've never worked with before—competent until proven otherwise, and worth the extra five minutes it takes to actually verify.