●MAX — Rork Max generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●PUBLISH — Rork Max ships 2-click App Store publishing and runs $200/month●RN — The standard Rork builds native iOS/Android apps with React Native (Expo) — the quicker path to a working app●PRICE — Rork is free to start, with paid plans from $25/month●FUND — Rork raised $2.8M from a16z; the platform now sees 743k+ monthly visits with 85% growth●FLOW — Describe your app in plain English and Rork generates deployable code that can use the camera, notifications, and more●MAX — Rork Max generates native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●PUBLISH — Rork Max ships 2-click App Store publishing and runs $200/month●RN — The standard Rork builds native iOS/Android apps with React Native (Expo) — the quicker path to a working app●PRICE — Rork is free to start, with paid plans from $25/month●FUND — Rork raised $2.8M from a16z; the platform now sees 743k+ monthly visits with 85% growth●FLOW — Describe your app in plain English and Rork generates deployable code that can use the camera, notifications, and more
Bugs Rork Can Fix vs. Bugs You Should Fix Yourself: A Triage Workflow for Exported Code
A practical triage workflow for telling apart the bugs Rork resolves on its own from the ones you should hand-fix in exported React Native/Expo code, with working examples.
The thing I noticed building apps with Rork is that most bugs really do get fixed when you just tell the chat to fix them. Real-world reviews bear this out: roughly 70% of the bugs people hit were resolved with no hands-on work, while the remaining 30% needed manual repair in the exported codebase.
How you treat that 30% decides whether Rork becomes a reliable partner or a tool you abandon halfway. From years of running my own apps as an indie developer, I've come to feel that the situations where handing everything to the AI stalls are precisely the ones that belonged on the "human fixes this" side. This article lays out a triage routine for drawing that line, plus the five fixes that actually recur most often in exported React Native (Expo) code — each with working code.
Where the line falls between Rork's bugs and yours
For context: Rork is a mobile-only AI app builder whose standard plan generates native iOS / Android apps in React Native (Expo). Because you can export the generated code, the final repair can happen on your machine or through the chat.
In practice, the line falls like this. Rork is good at bugs you can reproduce on screen and describe in words. A button that does nothing, a broken layout, navigation that goes somewhere unexpected — say what you see and it usually gets fixed. What you should fix yourself are bugs that are intermittent, or only appear on a specific device, OS, or network state. The AI has little to reproduce these from, so you end up going in circles no matter how many times you message back and forth.
The deciding question is: "Can I describe how to reproduce this bug in three sentences in the chat?" If you can, delegate it. If you can't, look at it yourself. That single rule alone removes a lot of wasted round trips.
The first move in triage: split errors by layer
Before touching any code, figure out which layer the bug lives in. Once you know the layer, whether to delegate or hand-fix follows automatically.
Layer
Typical symptom
First choice
UI / layout
broken, unresponsive, wrong color
Delegate to Rork (you can describe the symptom)
Navigation
back behavior, deep links, tab transitions
Rork first; if repro is complex, do it yourself
Async / API
occasional freeze, silent failure
Fix it yourself (eyeball the swallowed errors)
Platform divergence
works on iOS, crashes on Android
Fix it yourself (verify branches on a device)
State / persistence
lost on restart, stale value remains
Fix it yourself (trace save and restore)
The bottom three layers — async, platform divergence, persistence — are exactly what that "30%" is made of. From here we'll walk through the five patterns I hand-fix most often, centered on those three layers.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can separate the bugs Rork fixes on its own from the ones you must hand-fix, cutting the time you spend going in circles
✦You'll be able to apply five recurring fixes for exported React Native/Expo code today, with complete working code
✦You can make a generate → verify → repair triage routine part of your own project and confidently widen what you let the AI handle
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The most common thing in generated code is a try/catch that swallows errors. The AI is good at writing "code that doesn't crash," so the catch block is often empty, or stops at a console.log. In production this becomes "occasionally freezes" or "fails silently" — and because you can't describe the repro, handing it to Rork won't fix it.
// Before: a common shape right after generation. The failure is swallowed.async function loadProfile(userId: string) { try { const res = await fetch(`https://api.example.com/users/${userId}`); const data = await res.json(); setProfile(data); } catch (e) { console.log(e); // ← it ends here. The user is told nothing. }}
What needs hands-on attention here is checking the HTTP status and reflecting failure into user-visible state. fetch does not throw on 404 or 500, so you have to check res.ok yourself.
// After: check the status, and hold failure as statetype LoadState = | { status: "loading" } | { status: "success"; profile: Profile } | { status: "error"; message: string };async function loadProfile(userId: string): Promise<LoadState> { try { const res = await fetch(`https://api.example.com/users/${userId}`); // fetch does not reject on 4xx/5xx — check explicitly if (!res.ok) { return { status: "error", message: `Server error (${res.status})` }; } const profile: Profile = await res.json(); return { status: "success", profile }; } catch (e) { // We reach here for network-unreachable, malformed JSON, etc. return { status: "error", message: "Connection failed. Please check your signal." }; }}
The reason for returning a state object is that the screen can then always render "loading / success / failure" distinctly. Turning a swallowed error into a visible failure alone sharply reduces vague "it's sometimes weird" reports from users.
Fix #2: The spots where iOS and Android diverge
Cross-platform generated code is frequently verified on only one OS. I run a wallpaper app on both iOS and Android myself, and the places where identical code behaves differently are almost always the same three: the safe area, the back button, and the keyboard.
Android's hardware/gesture back in particular slips right through when the generated code only assumes the iOS swipe-back. Here is an example of catching the back press yourself while a modal is open.
import { useEffect } from "react";import { BackHandler, Platform } from "react-native";// Map the Android back press to "close" while a modal is openfunction useAndroidBackToClose(isOpen: boolean, onClose: () => void) { useEffect(() => { if (Platform.OS !== "android" || !isOpen) return; const sub = BackHandler.addEventListener("hardwareBackPress", () => { onClose(); return true; // return true to suppress the OS default "exit app" }); return () => sub.remove(); }, [isOpen, onClose]);}
The key is return true. Forget it and you get an Android-only mystery where closing the modal also sends the app itself to the background. It never reproduces in the iOS simulator, so telling Rork "it crashes when I go back" gives it nothing to reproduce. Keeping one physical Android device on your desk and checking it yourself is, in the end, the fastest path — that's my conclusion.
Fix #3: The list that stutters while scrolling
In lists past a few dozen items, generated code that lays every element into a ScrollView makes scrolling heavy. Rork is good at building the look, but the initial generation sometimes doesn't account for performance as counts grow. You can only call this "it stutters," which is hard to describe, so swapping in FlatList (or FlashList) yourself is the standard move.
import { FlatList } from "react-native";// Drop the ScrollView + map and use a virtualized list<FlatList data={items} keyExtractor={(item) => item.id} renderItem={({ item }) => <WallpaperCard item={item} />} // discard off-screen rows to curb memory and dropped frames removeClippedSubviews initialNumToRender={8} windowSize={5} // if row height is fixed, getItemLayout skips scroll math getItemLayout={(_, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index, })}/>
getItemLayout is usable only when row height is fixed, but its effect is large. For a grid-style app whose list items share a uniform height, just adding this removes the hitch on first scroll. On my wallpaper app's thumbnail list, this swap visibly changed how smooth the scroll feels.
Fix #4: The order of permission and ATT calls
Once you add ads or tracking, generated code sometimes doesn't manage the order in which permission dialogs appear. On iOS, mistiming the ATT (App Tracking Transparency) prompt loses measurement consent and hurts revenue. The rule of order is: tracking permission → ad SDK initialization.
import { requestTrackingPermissionsAsync } from "expo-tracking-transparency";// Always settle ATT before initializing the ad SDKasync function initAdsWithConsent() { const { status } = await requestTrackingPermissionsAsync(); const personalized = status === "granted"; // Only now initialize the ad SDK. // Pass personalized; fall back to non-personalized ads if denied. await initMobileAds({ requestNonPersonalizedAdsOnly: !personalized });}
Reverse the order and the SDK initializes with a pre-consent status — and there's no taking it back afterward. This surfaces late as "revenue isn't growing," a kind of bug whose cause is very hard to pin down. That's exactly why it's worth verifying the order yourself right after generation. If you want to go a layer deeper on this, see the companion piece on cold-start ordering of consent, billing, and ads in Rork apps.
Fix #5: Persisting and restoring state
Reports like "my settings vanished after a restart" or "an old value is still there" are almost always a missing persistence-layer fix. Generated code builds the in-memory state but can forget the save and restore. Because saving to AsyncStorage is async, watch for the race where the screen renders with default values before the read completes.
import AsyncStorage from "@react-native-async-storage/async-storage";import { useEffect, useState } from "react";function usePersistentSetting(key: string, fallback: string) { const [value, setValue] = useState(fallback); const [hydrated, setHydrated] = useState(false); // restore-complete flag // restore once on launch useEffect(() => { AsyncStorage.getItem(key) .then((stored) => { if (stored !== null) setValue(stored); }) .finally(() => setHydrated(true)); }, [key]); // save on change (only after restore completes) useEffect(() => { if (hydrated) AsyncStorage.setItem(key, value); }, [key, value, hydrated]); return { value, setValue, hydrated };}
The hydrated flag exists to prevent the accident where a save runs before restore finishes and overwrites with the default value. On the screen side, show a loading state until hydrated is true, or wait before rendering with defaults. This one small flag erases the bulk of that hard-to-reproduce "settings reset themselves sometimes" bug.
How to keep notes so you can widen what you delegate
We've gone through five fixes, but what truly pays off is the habit of recording the patterns you hand-fixed. The moment you make the same fix twice, that's an instruction to write into your Rork prompt up front next time. Tell it "always check res.ok on fetch responses" and "always handle the Android hardware back" from the start, and the share you can stamp out at generation time rises — the 30% you fix yourself shrinks to 20%, then 10%.
The AI builds the base and the human finishes it — that division of labor isn't fixed; through your own notes you can shift it toward the AI bit by bit. Start the next time you fix something in Rork by leaving a one-line note about the repair. That, more than anything, is the surest foothold for confidently widening what you let it handle.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.