Rork で4画面ほどのアプリを組んでいて、画面単体ではきれいに動くのに、画面をまたいだ瞬間に状態が消える。この壁に当たった方は少なくないのではないでしょうか。
私自身、個人開発で長く運営しているユーティリティ系アプリの社内向け管理ツールを Rork で作り直した際、一覧→詳細→設定と遷移するたびに選択中のフィルター条件がリセットされる症状にしばらく付き合うことになりました。
原因はアプリの規模ではありませんでした。プロンプトの構造でした。
多画面アプリで Rork の生成が崩れはじめる典型パターンと、それを防ぐ設計・プロンプトの分割方法を、実際に動いた構成とあわせて共有いたします。
なぜ「多画面になった途端」に壊れるのか
Rork のバイブコーディングは、1画面ずつなら驚くほど正確です。崩れるのはいつも画面の「間」です。
理由ははっきりしています。Rork は修正指示のたびに、該当する画面のコードを丸ごと再生成することがあります。このとき、画面コンポーネントの中に API 呼び出しや状態のロジックを書かせてしまっていると、再生成のたびに他画面との接続部分が上書きされて壊れます。
つまり多画面アプリの守り方は、「再生成されても壊れない場所」にロジックを退避させておくことに尽きます。私は次の3層で指定しています。
- プレゼンテーション層(screens/): UI と表示ロジックのみ。ここは Rork に何度でも再生成させてよい場所
- ビジネスロジック層(context/・hooks/): 状態管理とデータ変換。再生成の対象から外すよう明示する場所
- データ層(services/): Firestore / Supabase / Stripe との通信。一度固めたら触らせない場所
この分離をプロンプトの冒頭で宣言しておくと、Rork は画面を作り直すときも context/ と services/ を温存する傾向がはっきり強まります。体感では、この一文があるかないかで手戻りの回数が目に見えて変わりました。
プロンプトは「構造 → 画面 → 接続」の3段階に分ける
最初に全画面を一気に指定したくなりますが、4画面を超えるアプリでは一括生成は勧めません。私が現在使っている手順は3段階です。
第1段階: 構造だけを先に固定する
このアプリは次の構造で作ってください。
- screens/ には UI のみを置き、API 呼び出しを書かないでください
- 状態は context/ に Context + useReducer で集約してください
- 外部サービスとの通信は services/ にまとめてください
まず空の画面4つとナビゲーションだけを作ってください。
第2段階: 画面を1枚ずつ埋める
eコマースアプリなら、商品一覧(Firestore からカテゴリフィルター付きで取得・カード表示)、商品詳細(説明・レビュー・在庫と「カートに追加」)、カート(数量の増減と小計)、チェックアウト(住所フォームと Stripe 連携)を、この順で1画面ずつ指示します。
第3段階: 接続を明示する
商品一覧で選択したカテゴリフィルターは、詳細画面へ遷移して戻っても
保持してください。この状態は ProductContext に置き、
画面コンポーネント内のローカル state にしないでください。
冒頭で触れたフィルター消失は、まさにこの第3段階を省略したのが原因でした。Rork は「フィルター機能を付けて」とだけ言うと、画面ローカルの useState で実装します。ローカル state は画面がアンマウントされた時点で消えるので、遷移して戻ると初期化されます。どの状態を画面をまたいで生かすのかは、こちらが文章で宣言しない限り伝わりません。
生成後のフォルダ構成は、おおむね次の形に収束します。
src/
├── screens/
│ ├── ProductList.tsx
│ ├── ProductDetail.tsx
│ ├── ShoppingCart.tsx
│ └── Checkout.tsx
├── context/
│ ├── CartContext.tsx
│ └── ProductContext.tsx
├── services/
│ ├── firestoreService.ts
│ └── stripeService.ts
└── navigation/
└── RootNavigator.tsx
状態管理は「型まで」プロンプトで指定する
複数画面で共有する状態(ログイン情報・カート・フィルター条件)は Context + useReducer に集約しますが、ここで手を抜かずアクションの型まで指定するのが、後で効いてきます。
CartContext は次の型で実装してください。
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" };
reducer は必ず新しい配列を返し、state.items を直接変更しないでください。
最後の一文は経験からの追加です。以前、数量変更だけ画面に反映されない症状を追いかけたところ、生成された reducer が state.items[i].quantity = n と直接書き換えていて、参照が変わらないため再レンダリングが走っていませんでした。型と合わせて「直接変更しない」まで書いておくと、この種の再現しにくいバグを最初から踏まずに済みます。同じ症状の切り分けはstate が更新されない・再レンダリングされない問題の対処にも詳しくまとめています。
楽観的更新はロールバックとセットで頼む
「カートに追加」のような操作は、体感速度のために楽観的更新にしたいところです。ただし Rork に「楽観的更新にして」とだけ伝えると、成功パスしか実装されないことがあります。失敗時の巻き戻しまでワンセットで指示します。
「カートに追加」は次の順で処理してください。
1. まず UI とローカルの CartContext に即時反映する
2. 並行して Firestore に非同期で保存する
3. 保存に失敗したら CartContext から該当商品を取り除き、
「追加できませんでした」とトーストで表示する
3 まで書いて初めて、電波の悪い環境でカートと Firestore が食い違う事故を防げます。個人開発では自分ひとりが最初のテスターなので、地下鉄で自分のアプリを触っていて気づいた、という発見が実際に何度かありました。
再生成で壊さないための運用ルール
多画面アプリを Rork で育てるうえで、最終的に一番効いたのは設計そのものより運用ルールでした。私は次の2つを守っています。
修正指示に「触ってよい範囲」を毎回書く。 「詳細画面のレイアウトを直してください。context/ と services/ は変更しないでください」のように、スコープを一文添えます。地味ですが、動いていた接続が巻き添えで壊れる確率が下がります。
接続が絡む変更の前に、動作している状態を書き出しておく。 エクスポートやバージョン履歴で「全画面が繋がって動く地点」を確保してから、次の大きな指示を出します。壊れたら直すのではなく、壊れたら戻れるようにしておく、という順番です。
アーキテクチャのパターン自体をもっと深く比較したい方は、MVVM と Clean Architecture を Rork でどう使い分けるかが次の一歩として繋がるはずです。
まずは今作っているアプリで、画面をまたいで生き残ってほしい状態を紙に3つ書き出し、それを Context に移すプロンプトから試してみてください。多画面の崩れ方が変わるのを実感できると思います。