●MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode required●STACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decision●FOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generation●BUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebase●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●PRICING — It is free to start, with paid plans from $25/month, so you can try before committing●MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode required●STACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decision●FOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generation●BUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebase●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●PRICING — It is free to start, with paid plans from $25/month, so you can try before committing
Spotting the 30% of Bugs Rork Can't Fix Itself — A Hand-Fix Workflow Built Around Export
Rork resolves roughly 70% of the bugs it hits on its own; the remaining 30% needs your hands. Here is the criteria I use to decide whether to keep re-prompting or export and fix it myself, plus working code for the fixes.
When you build with Rork, it patches a surprising share of the errors you hit without your help. Published hands-on reviews put the number around 70% — bugs resolved with no manual intervention. The trouble is the other 30%. If you keep believing "one more prompt will fix it," you can watch the same patch loop around until morning. Across years of running apps as an indie developer, the thing that has burned the most of my time is exactly this: the 30% that looks fixable but isn't.
This article shares how I spot that 30% early, and how I draw the line between cases worth another prompt and cases that call for exporting the code and fixing it by hand. I also walk through two bugs that tend to survive in generated code, with the working code I used to root-cause them.
Where the easy 70% ends and the hard 30% begins
In practice, Rork is good at fixing "problems that live inside one screen, with visible state." It struggles with "problems that depend on timing or the outside environment, where the trigger is hard to put into words." The dividing line looks like this:
Rork usually self-fixes
Usually lands on your desk
Broken layouts and style mismatches
Async races (a stale response overwrites newer state)
Type errors and undefined references the compiler flags
Crashes that appear only on a specific device or OS version
Logic fixes inside a single component
Behavior that spans native SDKs — permissions, billing, push
Copy, constants, and default values
Off-by-one and index drift during list operations
The gap comes from what Rork is: a tool that fixes based on "the code it can see and your description." When the trigger hides in a timeline or external state, it never makes it into the description, so Rork applies a surface-level patch instead. That is why spotting the 30% starts with one question: is the cause inside the screen, or out in time and devices?
Keep re-prompting, or fix it by hand?
The test I actually use is simple. If two or more of the following are "yes," I stop re-prompting and switch to export.
After two prompts on the same symptom, has Rork edited different places without fixing it?
Is the error a stack trace with the cause already pinpointed?
Does the fix span multiple files where you want to keep things consistent at once?
Is it in billing, permissions, or push — territory that only reproduces in production?
Stacking more prompts onto a bug that matches two or more of these has nearly always been wasted time for me. If none match, letting Rork handle it is usually faster. As a table:
Situation
What to do
0–1 matches
Re-prompt Rork (describe the expected behavior, not the symptom)
2–3 matches
Export and fix only the root cause by hand
4 matches
Export, then rethink the design of the feature itself
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
✦Apply a concrete test, starting today, to separate the 70% of bugs Rork fixes automatically from the 30% you should fix by hand
✦Root-cause the bugs that tend to linger in generated code — async races and list-boundary crashes — with code you can run
✦Build a workflow, grounded in running apps at 50-million-download scale, so your manual fixes don't get reverted on the next regeneration
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.
Once you decide to fix by hand, hunting for where side effects scattered through the generated code is draining to repeat. Right after export, I add one light tidy-up: pull the touch points into one place. Concretely, I isolate async work and external I/O into a custom hook, leaving the screen component to just call it.
// hooks/useArticles.ts// Generated code tends to inline fetch inside the component.// Move it into a hook first, so the thing you edit lives in one file.import { useEffect, useRef, useState } from "react";type Article = { id: string; title: string };export function useArticles(query: string) { const [data, setData] = useState<Article[]>([]); const [loading, setLoading] = useState(false); useEffect(() => { setLoading(true); fetch(`https://api.example.com/articles?q=${encodeURIComponent(query)}`) .then((r) => r.json()) .then((json) => setData(json.items)) .finally(() => setLoading(false)); }, [query]); return { data, loading };}
There is still a bug here (fixed in the next section). What matters is that the screen stays a one-liner — const { data, loading } = useArticles(query); — confining the blast radius of any fix to the hook. The pattern of tidying generated code toward production quality mirrors the thinking in refactoring patterns for taking Rork Max's generated code to production quality.
Example 1: stopping an async race Rork kept looping on
The hook above has a flaw: switch the input quickly and a stale response overwrites newer state. On screen it shows up as "I cleared the search but the old results stayed." Telling Rork "the search results are stale" only ever produced tweaks to the loading indicator or the sort order — never the underlying race, because the cause lives in the timeline and never reaches the description.
To fix it by hand, give each request a sequence number and accept only the result of the last one fired.
// hooks/useArticles.ts (fixed)import { useEffect, useRef, useState } from "react";type Article = { id: string; title: string };export function useArticles(query: string) { const [data, setData] = useState<Article[]>([]); const [loading, setLoading] = useState(false); const latestReq = useRef(0); // sequence number of the newest request useEffect(() => { const reqId = ++latestReq.current; // id for this run setLoading(true); fetch(`https://api.example.com/articles?q=${encodeURIComponent(query)}`) .then((r) => r.json()) .then((json) => { // Drop the result unless we are still the newest request if (reqId === latestReq.current) { setData(json.items); } }) .finally(() => { if (reqId === latestReq.current) setLoading(false); }); }, [query]); return { data, loading };}
Holding latestReq in a useRef keeps the value across re-renders while updating it without triggering a redraw. You could use an AbortController to actually cancel the request, but deciding whether to accept a result by sequence number still works against older APIs that don't support cancellation, which is why I lean on it in production.
Example 2: root-causing a list-boundary crash with a defensive copy
The other bug that tends to linger in generated code is index drift in list operations. In a "remove from favorites" handler, mutating the array directly let the redraw and an index lookup fall out of sync and crash. In a wallpaper app I run, a structurally similar crash appeared only on certain devices, and pinning down the cause took real effort.
The fragile shape mutates the state array while another spot keeps referencing a stale index.
// Fragile: splice the state array directly; another reference driftsfunction removeFavorite(index: number) { favorites.splice(index, 1); // mutates state in place setFavorites(favorites); // same reference, so redraw is unstable setSelected(favorites[index]); // reads the now-shifted index -> undefined}
Root-cause it by building a new array every time and switching the reference to a value-based one.
// Fixed: defensive copy + value-based referencefunction removeFavorite(id: string) { setFavorites((prev) => { const next = prev.filter((f) => f.id !== id); // return a new array return next; }); // Decide selection by id, not by index setSelected((cur) => (cur?.id === id ? null : cur));}
Two points. Update state with an updater function (prev => next) so you always return a new array, and reference elements by id rather than index. An index loses meaning every time the order changes, but an id is bound to the element itself, so it holds up under deletion and reordering. For this class of bug, Rork often applies a band-aid — adding a range check on index. A boundary check makes crashes less likely, but the drifted-selection symptom remains, so changing how the reference is held was the real fix.
Keeping manual fixes from being reverted on regeneration
After you export and fix by hand, going back to Rork to add a feature can overwrite that hard-won fix. To avoid it, I treat the hand-fixed code as a "do not regenerate" zone at the workflow level. The method is plain.
First, commit hand-fixed files separately, with a message that says "manual fix: do not regenerate." Second, when asking Rork to add something, treat the fixed hook or function as already provided and instruct only how to call it. Hand it the interface, not the internals: "the useArticles hook is complete; use it to build the list screen."
Since switching to this, the drain of the same race bug returning on every regeneration has almost disappeared. Making the 30% of hand-fixing a one-time job sharply lowers the mental load.
Shrinking the 30% by design
Finally, a way to reduce the total volume of hand-fixing. What helped me most was naming the "fragile conditions" up front, in the very first prompt. Adding lines like "search inputs switch quickly, so never show stale results" and "manage list deletion by item id, not by index" raises the odds that the generated code avoids races and boundary drift from the start.
Rather than expecting perfect generation, calling out the fragile 30% by name ahead of time cut my rework in the end. Starting today, take one of your recent bugs and run it through the four questions at the top. Whether to keep re-prompting or to export will resolve itself faster than you'd expect.
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.