There is a moment in most Rork projects where things quietly change. Up to three screens, everything just works. Somewhere around screen four, a new kind of bug appears: each screen behaves perfectly on its own, but the moment you navigate between them, state disappears.
I ran into this while rebuilding an internal management tool for the utility apps I operate as an indie developer. Moving from the list view to a detail view and back would silently reset the active filter, every single time.
The cause turned out to have nothing to do with app size. It was the structure of my prompts.
Here is the failure pattern I kept hitting, and the design and prompting workflow that has held up since — along with the folder structure that actually shipped.
Why apps break precisely when they become multi-screen
Rork's vibe coding is remarkably accurate one screen at a time. The breakage always happens between screens.
The reason is concrete: when you ask for a revision, Rork will often regenerate the affected screen wholesale. If your API calls and state logic live inside screen components, every regeneration risks overwriting the connective tissue that other screens depend on.
So the defensive move for multi-screen apps is simple to state: keep logic in places that survive regeneration. I declare three layers up front.
- Presentation (screens/) — UI only. Rork is free to regenerate anything here.
- Business logic (context/, hooks/) — state management and data transforms. Explicitly excluded from regeneration.
- Data (services/) — Firestore / Supabase / Stripe communication. Once stable, hands off.
Declaring this separation at the top of a prompt measurably changes Rork's behavior: it becomes far more likely to preserve context/ and services/ when rebuilding a screen. In my experience, that single opening paragraph visibly reduced rework.
Split prompts into three stages: structure → screens → wiring
The temptation is to describe all screens in one big prompt. Past four screens, I no longer do this. My current workflow has three stages.
Stage 1: lock the structure first.
Build this app with the following structure:
- screens/ contains UI only; do not place API calls here
- All shared state lives in context/ using Context + useReducer
- All external service communication lives in services/
Start by creating four empty screens and the navigation between them.
Stage 2: fill in screens one at a time.
For an e-commerce app, that means the product list (Firestore fetch with category filters, card layout), product detail (description, reviews, stock, an add-to-cart button), the cart (quantity controls and subtotal), and checkout (address form plus Stripe) — each as its own prompt, in that order.
Stage 3: state the wiring explicitly.
The category filter selected on the product list must persist
when the user navigates to a detail screen and back.
Keep this state in ProductContext, not in local component state.
The vanishing filter I mentioned earlier was exactly a missing stage three. If you just say "add a filter," Rork implements it with a local useState — and local state dies when the screen unmounts. Which state must outlive navigation is something the model cannot infer; you have to say it in words.
The generated folder structure reliably converges on something like this:
src/
├── screens/
│ ├── ProductList.tsx
│ ├── ProductDetail.tsx
│ ├── ShoppingCart.tsx
│ └── Checkout.tsx
├── context/
│ ├── CartContext.tsx
│ └── ProductContext.tsx
├── services/
│ ├── firestoreService.ts
│ └── stripeService.ts
└── navigation/
└── RootNavigator.tsx
Specify shared state down to the types
Cross-screen state — auth, cart, filters — belongs in Context + useReducer. The step worth not skipping: spell out the action types in the prompt itself.
Implement CartContext with these types:
type CartItem = { productId: string; name: string; price: number; quantity: number };
type CartAction =
| { type: "ADD_ITEM"; item: CartItem }
| { type: "REMOVE_ITEM"; productId: string }
| { type: "SET_QUANTITY"; productId: string; quantity: number }
| { type: "CLEAR" };
The reducer must always return a new array. Never mutate state.items directly.
That last sentence comes from experience. I once chased a bug where quantity changes wouldn't render, and found the generated reducer doing state.items[i].quantity = n — mutating in place, so the reference never changed and React never re-rendered. Writing "never mutate" into the prompt prevents an entire class of hard-to-reproduce bugs before they exist. I've documented the diagnosis path for that symptom separately in fixing state that updates but never re-renders.
Ask for optimistic updates and rollback as one unit
Add-to-cart interactions deserve optimistic updates for perceived speed. But if you tell Rork only "make it optimistic," you may get the success path alone. Request the rollback in the same breath:
Handle "Add to Cart" in this order:
1. Reflect the item immediately in the UI and local CartContext
2. Persist to Firestore asynchronously in parallel
3. On failure, remove the item from CartContext and
show a toast: "Could not add item"
Step 3 is what prevents the cart and Firestore from drifting apart on a weak connection. As an indie developer you are usually your own first tester — more than once I've caught this exact failure while using my own app on the subway.
The operating rules that mattered more than the architecture
In the end, what kept multi-screen apps stable wasn't the design so much as two working habits.
Scope every revision request. "Fix the detail screen layout. Do not modify context/ or services/." One extra sentence, and the odds of collateral damage to working connections drop noticeably.
Snapshot a known-good state before any wiring change. Use export or version history to secure a point where all screens connect and run, then issue the next big instruction. Not "fix it when it breaks" but "make sure you can go back when it breaks."
If you want to go deeper on the architectural patterns themselves, choosing between MVVM and Clean Architecture in Rork is a natural next step.
For now, try this with the app you're building today: write down the three pieces of state that must survive navigation, then prompt Rork to move exactly those into Context. The way your app breaks — or stops breaking — will tell you the rest.