Rork でアプリを組んでいると、エラーが出ても Rork 自身がかなりの割合で直してくれます。公開されている実利用レビューでも、遭遇したバグのおよそ7割(約70%)は手を入れずに解決した、という数字が出ています。問題は残りの3割です。ここを「もう一回プロンプトで頼めば直る」と信じて粘り続けると、深夜に同じ修正がぐるぐる回るだけで朝を迎えます。私自身、個人開発で長くアプリを運用してきた中で、最も時間を溶かしたのはこの「直りそうで直らない3割」でした。
ここでは、その3割を早めに見抜き、再プロンプトで粘る場面とエクスポートして手で直す場面を切り分ける判断軸を共有します。あわせて、生成コードに残りがちな二種類のバグを、実際に動くコードでどう根治したかも示します。
自動修正が得意な3割と、苦手な3割の境目
まず体感として、Rork が自力で直しやすいのは「画面の中で完結する、状態の見えやすい不具合」です。逆に苦手なのは「タイミングや外部環境に依存し、再現条件が言葉にしづらい不具合」でした。境目を整理すると次のようになります。
自動修正が効きやすい 手直しに回りやすい
レイアウト崩れ・スタイルの不整合 非同期処理の競合(古いレスポンスが新しい状態を上書き)
型エラー・未定義参照などコンパイラが指摘するもの 特定端末・特定OSバージョンでのみ出るクラッシュ
単一コンポーネント内のロジック修正 権限・課金・通知などネイティブSDKをまたぐ挙動
文言・定数・初期値の変更 リスト操作中の境界・インデックスずれ
この差は、Rork が「いま見えているコードと説明文」を手がかりに直すツールであることに由来します。再現条件が時間軸や外部状態に隠れていると、説明文に書ききれず、Rork は表面的な対症療法を当ててしまいます。だからこそ、3割の見極めは「このバグの原因は画面の中にあるか、それとも時間や端末の外にあるか」を最初に自問することから始まります。
再プロンプトで粘るか、手で直すかの判断基準
私が実際に使っている切り分けは単純です。次の問いに二つ以上「はい」が付いたら、再プロンプトをやめてエクスポートに切り替えます。
同じ症状に対して Rork に2回頼んでも、別々の場所をいじって直っていないか
エラーメッセージがスタックトレース付きで、原因箇所が特定済みではないか
直すべき箇所が複数ファイルにまたがり、整合性を一括で取りたくないか
課金・権限・プッシュ通知など、App Store や Google Play の本番でしか再現しない領域ではないか
この4問のうち2つ以上が当てはまる不具合に再プロンプトを重ねるのは、ほぼ時間の浪費でした。逆に、1つも当てはまらないなら Rork に任せたほうが速いことが多いです。判断を表にするとこうなります。
状況 取るべき行動
該当0〜1個 Rork に再プロンプト(症状ではなく期待する挙動を具体的に書く)
該当2〜3個 エクスポートし、原因箇所だけを手で直す
該当4個 エクスポート後、該当機能の設計そのものを見直す
ノーコードと手書きの責務をどこで分けるかという土台の議論は、Rork Max の Swift 生成と Expo 版の責務分界 でも扱っています。あわせて読むと、3割の手直しをどのレイヤーで引き受けるかの判断がしやすくなります。
エクスポート後の手直しを最小化するプロジェクト構造
手で直すと決めたとき、生成コードのどこに副作用が散っているかを毎回探すのは消耗します。私は、エクスポートした直後に「触る場所を一箇所に寄せる」軽い整理を入れています。具体的には、非同期処理と外部I/Oをカスタムフックに隔離し、画面コンポーネントからは呼ぶだけにします。
// hooks/useArticles.ts
// 生成直後はコンポーネント内に直書きされがちな fetch を、まずフックへ隔離する。
// 手直しの対象を「この1ファイル」に固定できるのが狙い。
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 };
}
この時点ではまだバグが残っています(次の節で直します)。重要なのは、画面側を const { data, loading } = useArticles(query); の一行に保ち、修正の影響範囲をフック内に閉じ込めたことです。生成コードを本番品質へ寄せる整理の型は、Rork Max が生成したコードを本番品質まで磨き上げるリファクタリング・パターン に近い発想です。
実例1: 自動修正がループした非同期レースを止める
上のフックには、入力を素早く切り替えると古いレスポンスが新しい状態を上書きする欠陥があります。画面では「検索語を消したのに前の結果が残る」という形で現れます。Rork に「検索結果が古い」と伝えても、毎回ローディング表示や並び順をいじるだけで、根本のレースには届きませんでした。原因が時間軸にあるため、画面の説明文には載らないからです。
手で直す場合は、リクエストに通し番号を振り、最後に投げたものの結果だけを採用します。
// hooks/useArticles.ts(修正版)
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 ); // 最新リクエストの通し番号
useEffect (() => {
const reqId = ++ latestReq.current; // この実行のID
setLoading ( true );
fetch ( `https://api.example.com/articles?q=${ encodeURIComponent ( query ) }` )
. then (( r ) => r. json ())
. then (( json ) => {
// 自分が最新でなければ結果を捨てる(古いレスポンスの上書きを防ぐ)
if (reqId === latestReq.current) {
setData (json.items);
}
})
. finally (() => {
if (reqId === latestReq.current) setLoading ( false );
});
}, [query]);
return { data, loading };
}
latestReq を useRef で持つのは、再レンダリングをまたいでも値を保持し、かつ更新しても再描画を起こさないためです。AbortController で実際に通信を中断する手もありますが、結果採用の可否を通し番号で判定するこの形は、キャンセルに対応しない古いAPIでも効くので、運用では重宝しています。
実例2: リスト操作の境界クラッシュを防御的コピーで根治する
もう一つ、生成コードに残りがちなのがリスト操作の境界ずれです。お気に入り一覧から項目を消す処理で、配列を直接いじると、削除直後の再描画とインデックス参照がずれて落ちることがありました。私が運用している壁紙アプリでも、似た構造のクラッシュが特定端末でだけ出て、原因の特定に手間取った記憶があります。
落ちやすいのは、状態配列を破壊的に変更し、別の場所が古いインデックスで参照し続けるパターンです。
// 落ちやすい例: state の配列を直接 splice し、別の参照とずれる
function removeFavorite ( index : number ) {
favorites. splice (index, 1 ); // state を破壊的に変更
setFavorites (favorites); // 同一参照なので再描画が安定しない
setSelected (favorites[index]); // 削除でずれたインデックスを参照 → undefined
}
これを、毎回新しい配列を作り、参照は値ベースに切り替えて根治します。
// 根治版: 防御的コピー + 値ベースの参照
function removeFavorite ( id : string ) {
setFavorites (( prev ) => {
const next = prev. filter (( f ) => f.id !== id); // 新しい配列を返す
return next;
});
// インデックスではなく id で選択状態を判断する
setSelected (( cur ) => (cur?.id === id ? null : cur));
}
ポイントは二つあります。状態を更新関数(prev => next)で書き換えて常に新しい配列を返すこと、そして要素の参照をインデックスではなく id に変えることです。インデックスは並びが変わるたびに意味が崩れますが、id は要素そのものに紐づくので、削除や並べ替えに強くなります。Rork はこの種のバグに対し、しばしば index の範囲チェックを足すだけの対症療法を当てます。境界チェックは落ちにくくはしますが、選択がずれる症状は残るため、参照の持ち方そのものを変えるのが本筋でした。
手で直した修正を次の再生成で巻き戻さない
エクスポートして手で直した後、Rork に戻って機能追加を頼むと、せっかくの修正が上書きされることがあります。これを避けるため、私は手直しした箇所を「Rork に再生成させない領域」として運用上で固定しています。やり方はシンプルです。
第一に、手で直したファイルはコミットを分け、メッセージに「manual fix: 再生成禁止」と明記しておきます。第二に、Rork に追加を頼むときは、修正済みフックや関数を「すでに用意してある」前提で、その呼び出し方だけを指示します。「useArticles フックは完成しているので、それを使って一覧画面を作ってください」のように、中身ではなくインターフェースを渡すのです。
この運用に切り替えてから、再生成のたびに同じレースバグが戻ってくる、という消耗がほぼなくなりました。3割の手直しは一度きりの作業にできると、心理的な負担が大きく下がります。
3割を仕組みで減らす
最後に、手直しの総量そのものを減らす工夫です。私が効果を感じたのは、最初のプロンプトの段階で「壊れやすい条件」を先に言葉にしておくことでした。たとえば「検索は入力が速く切り替わる前提で、古い結果を表示しないこと」「一覧の削除は項目IDで管理し、インデックスに依存しないこと」と添えるだけで、生成コードがレースや境界ずれを最初から避ける確率が上がります。
完璧な生成を期待するより、壊れやすい3割をあらかじめ名指ししておくほうが、結果的に手戻りは減りました。今日からなら、まず自分の直近のバグ一つを、冒頭の4つの問いに当てはめてみてください。再プロンプトで粘るべきか、エクスポートすべきかが、驚くほど早く決まるはずです。