◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-03-22Advanced

Keeping Multi-Screen Apps From Falling Apart in Rork — A Three-Stage Prompting Approach to Screens, State, and Wiring

Once a Rork app grows past four screens, a familiar failure appears: every screen works on its own, but state vanishes the moment you navigate. The cause is prompt structure, not app size. Here is the three-layer separation, typed Context, and staged prompting workflow that fixed it for me.

Rork585Vibe Coding6Architecture22Multi-ScreenState Management8

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.

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.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-08-01
Every Experiment Kept Landing on the Same Devices: Hash Choice and Salt Position, Measured Across a Million IDs
On-device experiment assignment looked fine in isolation, then collapsed the moment two experiments ran side by side. Here is what one million IDs revealed about four hash functions, why the culprit was the concatenation order rather than hash quality, and the implementation I settled on, with the measured numbers.
⬡ Dev Tools2026-07-24
Collapsing Duplicate Requests Into One: A Reference-Counted Single-Flight Layer
When several components fire the same API call at launch, you get a burst of identical requests. Here is a single-flight layer that shares one in-flight promise instead: how to build the key that decides the folding, the trap of handing out a failed promise forever, the trap of one caller's abort cancelling everyone, and the test that keeps it all from regressing, with the real network numbers alongside.
⬡ Dev Tools2026-07-24
Resolving App Config in Three Layers: Merging Defaults, User, and Remote With Bounded Overrides
A single type-safe layer that merges compiled defaults, user preferences, and remote config. So a broken remote value never takes your app down, each key gets its own override strength, plus schema validation and range clamping, built from a real production incident.