Skip to main content
· 11 min readAIRN
Malik Chohra

By Malik Chohra

Avoid AI Code Slop in React Native: 6 Patterns + the Fix

About 80% of the slop Claude produces on a React Native codebase falls into 6 predictable patterns. You can catch all 6 with a six-line file at the root of your repo. Drop it in, restart the session, and the model self-corrects before you open a PR.

If you have come here looking for “review every AI line by hand or you will regret it,” that is half the answer. The other half: about 80% of the slop Claude produces on a React Native codebase falls into 6 predictable patterns. You can catch all 6 with a six-line file at the root of your repo. I call it .claude/slop-register.md. Drop it in, restart the session, and the model self-corrects on the most common failure modes before you ever open a PR.

The 6 patterns this article covers, by name, so you know what you are signing up for: Hermes ReadableStream that does not exist, useEffect chains around onLayout for animation timing, inline styles instead of theme tokens, Image from react-native instead of expo-image, RevenueCat entitlement string vs dashboard ID mismatch, and MMKV synchronous reads in the render path. If any of those made you wince, this article is for you.

💡 Pre-installed

AI Mobile Launcher Standard ships the slop-register plus the architecture and design system already wired in. Open the repo, restart your Claude Code session, ship.

The steel-man: “AI code is always slop, you have to review every line”

The case I am about to refute, stated fairly. Coding LLMs in 2026 are pattern-matchers over massive corpora. They produce code that looks plausible because it averages over what the training set looked like. They have no way to know what your specific repo already decided. So every diff is a potential bug, and the only safe move is full human review.

This is true at the limit. It is also expensive enough that most teams skip it under deadline pressure, then ship the slop anyway. The question is not “should I review AI code.” The question is “what is the cheapest review that catches 80% of the slop.”

My answer: the model is the cheapest reviewer if you tell it what to avoid. Models are very good at following instructions that sit in a file the session reads at startup. They are bad at inferring conventions from the code they have not yet opened. A .claude/slop-register.md flips the work from “human reviewer at 11pm” to “model self-correction at generation time.” The 6 patterns below cover most of what I keep flagging in code review across 4 React Native codebases.

Why a slop-register works (and what it actually is)

Claude Code reads the repo root for context files at session start. CLAUDE.md is the main one. A second file alongside it, .claude/slop-register.md, is a useful split because the register is a stop-list (do not do these things) and CLAUDE.md is an architecture index (here is how the code is shaped). Keeping them separate makes the register paste-portable across repos. Same register, every project.

The model treats the register as a high-priority constraint. When you ask for an image component, it reads “do not use react-native Image, use expo-image” before it generates. The wrong import never appears. The slop is not caught after the fact. It is not produced in the first place.

This is not theoretical. I have run the same prompts against the same boilerplate with and without the register file in the root. Without it, 4 of the 6 patterns below show up in the first generation. With it, all 6 are absent. The register is the difference between code review at PR time and code review at prompt time.

Pattern 1: Hermes ReadableStream that does not exist

Ask for a streaming completion from an LLM endpoint, and Claude produces this:

const response = await fetch(url, { method: "POST", body });
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value);
  // ...
}

It compiles. It looks correct. On Hermes it throws “TypeError: response.body.getReader is not a function” the moment you run it.

Web tutorials for OpenAI, Anthropic, and every LLM SDK show this exact pattern. The training set is heavily weighted toward browser code. The model has no signal that Hermes (React Native's default JS engine since 0.70) does not implement the ReadableStream API on response.body. So it produces the web answer.

Streaming features look like they work in tests because most tests stub fetch. They fail the first time a user opens the AI chat screen in a real build. Crash report, zero context, “TypeError” in a stack trace that points at a node_modules line. You spend the morning debugging your code before realizing the engine is missing the API.

The slop-register line that catches it: do not use response.body.getReader. Hermes does not implement ReadableStream. Use XMLHttpRequest with onprogress, or expo-fetch, or a callback-based native bridge (the llama.rn pattern).

Pattern 2: useEffect chains around onLayout for animation timing

Ask for a “slide in from the right” animation that starts after the element measures, and Claude produces:

const [width, setWidth] = useState(0);

const onLayout = (e: LayoutChangeEvent) => {
  setWidth(e.nativeEvent.layout.width);
};

useEffect(() => {
  if (width > 0) {
    Animated.timing(translateX, {
      toValue: 0,
      duration: 300,
      useNativeDriver: true,
    }).start();
  }
}, [width]);

Looks reasonable. Runs janky. The animation fires after a layout pass that already committed, which means the user sees one frame of the element in the wrong position before the slide begins. On lower-end Android, that one frame is 3 frames.

Measure-then-animate is the React-web idiom for cases where you need exact pixel values. It shows up in every “responsive React animation” blog post in the training set. The model does not know that React Native has react-native-reanimated shared values which run on the UI thread and never need a useEffect measurement.

The slop-register line: never measure-then-animate via useEffect + onLayout. Use react-native-reanimated shared values + useAnimatedStyle. UI thread, no React round trip.

Pattern 3: inline styles instead of theme tokens

Ask for a button component, get this:

<Pressable
  style={{
    backgroundColor: "#10B981",
    paddingVertical: 12,
    paddingHorizontal: 16,
    borderRadius: 8,
  }}
>
  <Text style={{ color: "#FFFFFF", fontWeight: "600", fontSize: 14 }}>
    {label}
  </Text>
</Pressable>

Inline styles, hex colors, magic numbers. In a codebase that already has a Restyle theme with theme.colors.primary, theme.spacing.md, theme.radii.sm, and a Box + Text primitive system, this component is a step backwards in maintainability and a guaranteed conflict in code review.

The Restyle pattern is overrepresented in production codebases and underrepresented in tutorials. Tutorials prefer inline styles because they are self-contained and explain themselves in one snippet. So the model defaults to what is easiest to demonstrate, not what the codebase already decided.

Dark mode breaks first. Then someone changes the primary color in theme.ts and the inline-styled component does not update. Then the design system migration that should take 2 hours takes 2 days because half the components do not read from tokens.

The slop-register line: never use inline style for colors, spacing, radii, or typography. Use theme tokens via Restyle (Box, Text) or the project's design-system primitives.

Pattern 4: Image from react-native instead of expo-image

Ask for an avatar component, get this:

import { Image } from "react-native";

<Image
  source={{ uri: user.avatarUrl }}
  style={{ width: 40, height: 40, borderRadius: 20 }}
/>

Works. Slow. No memory caching, no disk caching, no blurhash placeholder, no progressive loading. On a list of 100 avatars, scroll performance degrades and memory climbs.

react-native Image is in the older training corpus and has more example code. expo-image is newer and underrepresented even though it has been the default in any Expo app since 2024. The model picks the heavier-weighted import.

List screens with images get janky on scroll. Memory pressure on Android mid-range devices. CDN bills climb because the app re-fetches the same image on every screen visit. The fix is a one-line import swap. The detection is what costs you.

The slop-register line: import Image from expo-image, never from react-native. expo-image gives disk + memory cache, blurhash placeholders, and 2-3x faster list rendering.

Pattern 5: RevenueCat entitlement string vs dashboard ID mismatch

Ask for a paywall gate, get this:

import Purchases from "react-native-purchases";

const customerInfo = await Purchases.getCustomerInfo();
const isPro = customerInfo.entitlements.active["pro"] !== undefined;

The string "pro" is hardcoded. It looks fine. It is also wrong half the time because RevenueCat entitlement IDs are configured per-project in the dashboard, and the tutorial the model copied this from used "pro" but your dashboard might use "ai_pro", "premium", or "aimoblauncher_pro_v2" because you renamed it during the pricing migration.

Every RevenueCat tutorial uses "pro" as the example entitlement. The model has no way to read your dashboard. It produces whatever string was most common in training data.

Users buy the subscription. The webhook fires. The local customerInfo shows the entitlement under a different key than the code checks. The paywall stays up after purchase. Refund requests come in. Trust evaporates faster than any other bug class because money is involved.

The slop-register line: never hardcode entitlement IDs as inline strings. Read from constants in revenuecat.config.ts. The dashboard ID is the source of truth.

Pattern 6: MMKV synchronous reads in the render path

Ask for a “remember the user's preference” hook, get this:

import { MMKV } from "react-native-mmkv";
const storage = new MMKV();

function UserPreferences() {
  const theme = storage.getString("theme") ?? "light";
  const lang = storage.getString("lang") ?? "en";

  return <Box>{/* ... */}</Box>;
}

MMKV is fast. So fast that “just call it in the component body” feels like the right answer. It is not. Synchronous reads on cold start block the JS thread before the first frame. With 5-10 reads in a single render, the time-to-first-frame on mid-range Android stretches past 200ms, which is enough to feel slow.

MMKV's marketing emphasizes synchronous reads as the differentiator over AsyncStorage. The model interprets “fast and synchronous” as “safe to call inline.” It is the right tool, used in the wrong place.

Cold start feels sluggish. You optimize image sizes and bundle splitting for a week before realizing the problem was 8 MMKV reads in 3 components.

The slop-register line: never call .getString / .getNumber / .getBoolean directly in a component body. Wrap reads in useMemo with an explicit dependency array, or read once in a useEffect with setState.

The full slop-register template (paste-ready)

Drop this in the root of any React Native + Expo project. Restart your Claude Code session so it picks the file up at startup.

# .claude/slop-register.md

Stop-list rules for AI code generation in this React Native + Expo codebase.
Read before producing any code.

1. Streaming: do not use response.body.getReader. Hermes does not implement
   ReadableStream. Use XMLHttpRequest with onprogress, expo-fetch, or a
   callback-based native bridge (llama.rn pattern).

2. Animations: never measure-then-animate via useEffect + onLayout. Use
   react-native-reanimated shared values + useAnimatedStyle.

3. Styles: never use inline style for colors, spacing, radii, or typography.
   Use theme tokens via Restyle (Box, Text) or the project's design-system
   primitives.

4. Images: import Image from "expo-image", never from "react-native".
   expo-image gives disk + memory cache, blurhash placeholders, and 2-3x
   faster list rendering.

5. RevenueCat: never hardcode entitlement IDs as inline strings. Read from
   constants in revenuecat.config.ts. The dashboard ID is the source of
   truth.

6. MMKV: never call .getString / .getNumber / .getBoolean directly in a
   component body. Wrap reads in useMemo with an explicit dependency array,
   or read once in a useEffect with setState.

Six lines. The whole register. The model reads it alongside CLAUDE.md and adjusts every generation accordingly.

What the register does not catch

The honest section. The register is an 80% solution. It does not catch:

  • Cross-feature refactors with implicit dependencies. If feature A subscribes to a Redux slice that feature B mutates, AI sometimes misses one side.
  • Business-logic errors. The register has no opinion on whether your math is right.
  • Anything model-context-window-bound. If the diff is large enough that the model drops the register from context, the rules silently stop applying.
  • Hallucinated package APIs that exist in real packages. For that, you still need types and tests.
  • Architecture-level decisions. “Should this be a hook or a component” is what CLAUDE.md answers, not the register.

The register is a stop-list, not a code review. It moves the cheapest bugs off your plate. The expensive bugs are still your job.

Why this beats “just review every line”

Across 4 React Native codebases over 6 months, I tracked the pre-commit AI bugs that the register catches:

  • Without register: about 7-9 register-class bugs per 20 generated diffs.
  • With register: about 1-2 per 20.
  • Time cost to install the register: ~4 minutes per repo.
  • Time cost of catching the same bugs in PR review: 15-30 minutes each.

A 4-minute investment removes hours of weekly review. The model becomes its own first-pass reviewer because you finally gave it the constraints your codebase already lives by.

Where to go next

If you want the register pre-installed with the architecture, design system, and SDD pipeline already wired in, AI Mobile Launcher Standard ships with it. The AI Pro tier adds the on-device LLM, Dynamic Onboarding via Wire RN, and the cloud LLM routing layer.

One issue per week on AI-native mobile development at codemeetai.substack.com.

FAQ

What is the difference between CLAUDE.md and .claude/slop-register.md?

CLAUDE.md is the architecture index. It tells the model what the codebase contains, what conventions exist, and where features live. The slop-register is a stop-list. It tells the model what not to produce. They are read together at session start. Keeping them separate makes the register portable across projects.

Does the slop-register work with Cursor or other AI coding tools?

The file is plain markdown and any tool that reads repo-root context files will pick it up. Cursor reads .cursorrules natively, so you would copy the same six rules into that file. The principle is tool-agnostic.

Will the model actually follow the rules?

In my logs, yes, for the six rules listed. Models follow short, concrete, “do not do X” instructions at startup with high reliability. Where they break: vague rules, conflicting rules, or rules that contradict overwhelming training data. The six above are concrete and non-contradictory enough that adherence is near-perfect.

Can I version-control the slop-register?

Yes. Commit .claude/slop-register.md to your repo. Anyone running Claude Code on the codebase, including teammates, picks up the same rules. The register is a property of the codebase, not the developer.

Why React Native specifically?

Web codebases need the same approach but the patterns differ. React Native has its own slop cluster (Hermes, native modules, the bridge), and the patterns above are tuned for that. The same article for a Next.js codebase would have six different rules. The register format applies anywhere.

Related reading