DEV Community

Hugo Rus
Hugo Rus

Posted on

The Code Review Checklist for AI-Generated React Native (10 Things Agents Get Wrong)

AI-generated React Native code has a specific failure profile. It compiles. It renders. The screenshot looks right. And then a reviewer who's shipped a few apps opens the PR and finds the same ten problems they found in the last one.

That predictability is useful. If agents make the same mistakes, you can review for them mechanically, and you can stop most of them before they're written. This is the checklist I use when reviewing Claude Code, Cursor, or Codex output in Expo projects, with the fix for each and, where possible, a lint rule that catches it automatically.

1. Data fetching inside useEffect

The most common pattern in generated code:

useEffect(() => {
  supabase.from('notes').select('*').then(({ data }) => setNotes(data ?? []));
}, []);
Enter fullscreen mode Exit fullscreen mode

No loading state, no error state, no refetch, no cache, and a race condition if the component remounts. The fix is a query library:

const { data: notes, isLoading, error } = useQuery({
  queryKey: ['notes'],
  queryFn: async () => (await supabase.from('notes').select('*')).data ?? [],
});
Enter fullscreen mode Exit fullscreen mode

Catch it: an ESLint no-restricted-syntax rule that flags supabase.from inside useEffect, or a CLAUDE.md line: "All data fetching goes through TanStack Query hooks in src/queries/."

2. Secrets in EXPO_PUBLIC_ variables

Agents fix a failing API call by moving the key to where it "works":

EXPO_PUBLIC_OPENAI_KEY=sk-...   # shipped to every user's device
Enter fullscreen mode Exit fullscreen mode

Anything prefixed EXPO_PUBLIC_ is inlined into the JS bundle. Third-party keys belong in an Edge Function; the app calls the function with the user's session.

Catch it: a CI grep for EXPO_PUBLIC_.*(KEY|SECRET|TOKEN) that fails on matches, plus a deny rule in .claude/settings.json for reading .env.

3. Tables created without RLS

Generated migrations create the table and stop:

create table public.notes (...);
-- no "alter table ... enable row level security"
Enter fullscreen mode Exit fullscreen mode

On mobile, RLS is the only security boundary, because the client key ships inside the binary.

Catch it: a CI query that fails if any public table has rowsecurity = false, and a deny-case test per table.

4. any everywhere, or no types at all

const handleSubmit = (data: any) => { ... }
Enter fullscreen mode Exit fullscreen mode

Generated code reaches for any the moment a type gets inconvenient, which hides exactly the bugs TypeScript exists to catch.

Catch it: @typescript-eslint/no-explicit-any as an error, and npx tsc --noEmit in the agent's definition of done.

5. Inline styles mixed with the styling system

Half the components use NativeWind classes, half use style={{ marginTop: 12 }}, and a few use StyleSheet.create. Each one was locally reasonable; together they're unmaintainable.

Catch it: react-native/no-inline-styles from eslint-plugin-react-native, and a single line in the agent instructions naming the one system you use.

6. Lists without stable keys and without FlashList

{items.map((item, i) => <Card key={i} ... />)}
Enter fullscreen mode Exit fullscreen mode

Index keys break reordering and deletion. A .map inside a ScrollView renders every item at once. Long lists need FlatList at minimum, and FlashList for anything that scrolls fast.

Catch it: react/no-array-index-key, and a review rule: any list that can exceed ~20 items uses a virtualized list.

7. No empty, loading, or error states

Generated screens show the happy path. First launch with no data renders a blank screen; a failed request renders nothing.

Catch it: a review checklist item, not a lint rule. Every screen that fetches needs three extra states. Ask the agent for them explicitly: "include loading, empty, and error states."

8. Keyboard covers the input

Forms work in the simulator and fail on a real device: the keyboard hides the submit button, and nothing scrolls.

Fix: KeyboardAvoidingView with behavior="padding" on iOS, or react-native-keyboard-controller for anything more than a login form.

Catch it: test every form on a physical device with the keyboard open. No lint rule saves you here.

9. Navigation imported from the wrong package

In Expo Router projects (SDK 56+), agents still import useNavigation from @react-navigation/native, which now creates a second navigation context and breaks routing.

Catch it: no-restricted-imports for @react-navigation/*, and the instruction "navigation comes from expo-router only."

10. Hardcoded strings and magic numbers

'Something went wrong' in twelve places, #3B82F6 in thirty, 1000 as a timeout with no name.

Catch it: a theme.ts and a strings.ts (or i18n setup) that exist before the agent starts, so it has somewhere to put them. Agents copy the structure they see.

The pattern behind the pattern

Look at the fixes: most of them are structure that existed before the agent wrote a line. A queries folder. A theme file. RLS in the migration template. Lint rules that fail the build. An instruction file that names the one way to do each thing.

Agents don't write bad code because they're bad at code. They write the code the codebase implies. Give them a codebase that implies good code, and the checklist mostly stops firing.

That's the thesis behind AppLighter: Expo + Supabase templates where the structure above (queries, theme, RLS migrations, lint config, agent instructions) is already in place, so Claude Code or Cursor extends a good codebase instead of inventing a mediocre one.

The checklist, copy-pasteable

  • [ ] No data fetching in useEffect; queries live in hooks
  • [ ] No secrets in EXPO_PUBLIC_; third-party keys in Edge Functions
  • [ ] Every new table enables RLS in the same migration
  • [ ] No any; tsc --noEmit passes
  • [ ] One styling system, no inline styles
  • [ ] Stable keys; virtualized lists for anything long
  • [ ] Loading, empty, and error states on every fetching screen
  • [ ] Forms tested with the keyboard open on a device
  • [ ] Navigation from expo-router only
  • [ ] Strings and tokens in strings.ts / theme.ts

What's on your list that isn't on mine? I'd bet the eleventh item is different for every team.

Top comments (0)