RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-05-17中級

RorkのFlatListが突然クラッシュする - Stateの直接変更が原因のパターンと防御的コピー修正法

Rork生成のFlatListが特定操作でクラッシュする場合、配列Stateの直接変更が原因です。Beautiful HD Wallpapersで28日間50ユーザー超のクラッシュを根治した防御的コピーパターンを具体的に解説します。

FlatList9クラッシュ4state管理2React Native209デバッグ22防御的コピー

Google Play Consoleを開いたら、28日間で50ユーザー以上が同じスタックトレースでクラッシュしているのを見つけたことがあります。私が2014年から個人開発しているBeautiful HD Wallpapers(iOS/Android、累計5,000万DL超)のAndroid版v2.0.0リリース後のことでした。

スタックトレースをたどると、原因はあっけなく見つかりました。リスト表示に渡す配列を、直接書き換えていたのです。これはAndroidのRecyclerViewだけでなく、Rorkが生成するReact NativeのFlatListでもまったく同じ問題が起きます。私が実際に踏み、v2.1.0で解消したパターンを、同じ状況に直面した方に向けて具体的に紹介します。

なぜFlatListはこのパターンで落ちるのか

JavaScriptでは、配列や変数はオブジェクトへの「参照」を保持しています。const newList = items と書いても、新しい配列は作られず、同じメモリ上のオブジェクトを指す参照が増えるだけです。

// このコードは見た目は動いているが、危険なパターン
const handleCategoryChange = (newCategory) => {
  const filtered = allWallpapers; // 参照コピー(新しい配列ではない)
  filtered.sort((a, b) => a.category.localeCompare(b.category));
  setWallpapers(filtered); // Reactは「同じ配列だ」と認識してスキップすることがある
};

FlatListは内部でアイテム数とインデックスを追跡しています。Stateの参照が変わらないまま配列の中身だけが変わると、FlatListが保持する内部状態と実際のデータがずれます。高速スクロール時や素早い操作で、そのずれが Inconsistency detectedCannot read properties of undefined というクラッシュとして表れます。

開発環境では再現しにくく、本番の実機・実ユーザーで突発的に起きるため、対処が後手に回りやすいのもこのパターンの特徴です。端末の性能やネットワーク状況、ユーザーの操作速度によってタイミングが変わるため、「自分では全く再現しないのにPlay Consoleにはクラッシュが積み上がっている」という状況になりやすいです。

Rorkが生成するコードは構造がしっかりしていますが、そのコードを編集・拡張していく過程でこの問題が混入することがあります。特にAPIのレスポンスをそのままStateに入れていたり、既存の配列をループで加工してからセットしているコードに注意が必要です。 Reactの公式ドキュメントでは「Stateをイミュータブルに扱う」ことが基本原則として挙げられています。これは「Stateに格納したオブジェクトや配列を直接変更してはいけない、変更したい場合は新しいオブジェクト・配列を作ってから渡す」という意味です。FlatListのクラッシュはこの原則を破ったときに発生する代表的な症状のひとつです。個人開発を10年以上続けるなかで、この原則を体感として理解できたのは、実際にクラッシュレポートが50件を超えてからでした。特にAPIのレスポンスをそのままStateに入れていたり、既存の配列をループで加工してからセットしているコードに注意が必要です。

防御的コピーで根治する

修正の考え方はシンプルです。StateのSetterを呼ぶ前に、必ず新しい配列を作ることです。スプレッド演算子 [...array] を使うか、filtermap など新しい配列を返すメソッドを使うか、SetterにPrev関数を渡すかの3つが主な手段です。

// ✅ スプレッド演算子で新しい配列を作る
const handleCategoryChange = (newCategory) => {
  const filtered = [...allWallpapers].sort(
    (a, b) => a.category.localeCompare(b.category)
  );
  setWallpapers(filtered); // 新しい配列参照なのでReactが確実に検知する
};
 
// ✅ Setterに関数を渡すと常に最新のStateを受け取れる
const addItem = async () => {
  const newItem = await fetchItem();
  setItems(prev => [...prev, newItem]); // prevは呼ばれた時点の最新値
};
 
// ✅ filterやmapは新しい配列を返すので安全
const removeItem = (targetId) => {
  setItems(prev => prev.filter(item => item.id !== targetId));
};

[...array].sort()array.sort() は挙動が異なります。.sort() は元の配列を書き換えます(破壊的メソッド)。コピーを先に作ってから処理する点が肝心です。同様に、splicepush も元の配列を変更する破壊的メソッドです。これらを使う場面では、先にコピーを作ることを習慣にしてください。

Rork生成コードで見落としやすい3つのパターン

Rorkが出力するコードを編集するときに、意図せず混入しやすいパターンを3つ紹介します。実際に私がBeautiful HD Wallpapersのコードで踏んだものと、React Native版の等価パターンです。

パターン1: splice/pushによる破壊的操作

削除機能を追加するときに、直感的に splice を使うと問題が起きます。

// ❌ spliceは元の配列を変更する
const deleteItem = (index) => {
  const newItems = items; // 参照コピー
  newItems.splice(index, 1); // 元の配列を破壊的に変更
  setItems(newItems); // 同じ参照のためUIが更新されないこともある
};
 
// ✅ filterで新しい配列を生成する
const deleteItem = (index) => {
  setItems(prev => prev.filter((_, i) => i !== index));
};

filter を使うと、元の配列はそのままに、条件を満たす要素だけを持つ新しい配列が返ってきます。削除だけでなく、並び替えや絞り込みにも同じ発想が使えます。

パターン2: Object.assignによる浅いコピー漏れ

アイテムの一部フィールドを更新するときは、ネストしたオブジェクトにも注意が必要です。配列の各要素を更新する際に Object.assign を使うと、元のオブジェクトが変更されてしまいます。

// ❌ Object.assignは参照のシャローコピー。元のオブジェクトが変更される
const updateItem = (id, changes) => {
  setItems(prev => prev.map(item =>
    item.id === id ? Object.assign(item, changes) : item
  ));
};
 
// ✅ スプレッドで新しいオブジェクトを作る
const updateItem = (id, changes) => {
  setItems(prev => prev.map(item =>
    item.id === id ? { ...item, ...changes } : item
  ));
};

スプレッド { ...item, ...changes } は、item のすべてのプロパティをコピーした新しいオブジェクトを作り、そこに changes を上書きします。元の item オブジェクトには一切触れないので安全です。

パターン3: 非同期処理での古いStateへの参照

APIから取得したデータをStateにセットするとき、クロージャで古い items を参照してしまうケースです。これはクラッシュではなく「データが混ざる」バグとして現れることもあります。

// ❌ itemsはAPIコール開始時点の値。途中で別の更新があると古い値と合算される
const loadMore = async () => {
  const result = await fetchNextPage();
  setItems([...items, ...result.data]); // itemsが古い値かもしれない
};
 
// ✅ 関数形式にすれば常に最新値が渡ってくる
const loadMore = async () => {
  const result = await fetchNextPage();
  setItems(prev => [...prev, ...result.data]);
};

setItems(prev => ...) の形式にするだけで、Reactが呼び出し時点の最新のStateを prev として渡してくれます。非同期処理が絡む場面では、この書き方を基本にすると安全です。

修正後の結果と検出コマンド

Beautiful HD Wallpapersでは、v2.1.0でこの修正を全面的に適用しました。28日間で50ユーザー以上に発生していたクラッシュは、リリース後7日間でほぼゼロになっています。Android Vitalsの「ユーザー体験への影響が大きいクラッシュ」グラフが、修正後の計測期間ではフラットになっていることを確認しました。

Rorkプロジェクトで同様の問題がないかを確認するには、以下のコマンドが使えます。

# 直接代入して配列を使い回している箇所を検索
grep -rn "const new.*= items\|const new.*= data\|const new.*= list" src/ --include="*.ts" --include="*.tsx"
 
# splice/push/sort など破壊的操作の使用箇所
grep -rn "\.splice(\|\.push(\|\.sort(" src/ --include="*.tsx" | grep -v "// ✅"

これで出てきたファイルを順に確認し、Setter呼び出しの直前で元の配列を書き換えていないかを確認してみてください。危険な箇所が見つかったら、スプレッドか関数形式のSetterに置き換えるだけです。

この調査を一度実施しておくと、クラッシュが起きる前に潜在的な問題を発見できます。大規模な機能追加の前に走らせておくと、リリース後のAndroid Vitalsのグラフをきれいに保つ助けになります。特にカテゴリ切り替えやページング(追加読み込み)の実装後は、このチェックをルーティンとして取り入れることをお勧めします。

既存記事から内部リンク確認時の注意

FlatListのパフォーマンス問題が気になる方は、FlatListをFlashListに移行する手順を解説したRorkのFlashList v2移行でスクロール性能を改善するも参考になります。クラッシュが解消した後のパフォーマンス最適化の次のステップとして取り組むとよいでしょう。

Stateが更新されても画面に反映されない問題については、Rorkで状態が更新されない・再レンダリングされない問題の解決もあわせてご覧ください。今回解説した参照渡しの問題と密接に関連しています。

次にやること

今のRorkプロジェクトのコードで setXxx([...setXxx(prev => を使っていない箇所を一度検索してみてください。見つかった箇所が、次のクラッシュレポートの発生源になっている可能性があります。

個人開発を10年以上続けてきた中で感じるのは、このパターンへの対処は「正しいやり方を覚えたら終わり」の類の問題だということです。一度意識するだけで、以降のコードレビューで自然とチェックできるようになります。同じクラッシュで悩んでいる方の参考になれば幸いです。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-04-22
FlatList が空白で何も映らない—Rork アプリのリスト崩れを切り分ける
Rork で生成したアプリの FlatList が真っ白で何も表示されないときに、どこから疑うべきかを順番にまとめました。data・flex・keyExtractor・親要素の高さを見直すだけで直ることも多いです。
開発ツール2026-06-20
Rork の一覧が増えるほどスクロールで重複と欠落が出る——カーソルページネーションと再取得の状態設計
Rork が生成する素朴な offset ページネーションは、リストが更新されるたびに重複や欠落を起こします。カーソル方式の契約設計、取得状態を一つに束ねる usePaginatedList フック、失敗時の指数バックオフ再試行まで、本番運用で詰まらない一覧の作り方を実装中心に解説します。
開発ツール2026-06-20
Rork が直せるバグと自分で直すバグを見分ける — エクスポートコードのトリアージ手順
Rork が自力で直すバグと、エクスポートしたReact Native/Expoコードを自分で手当てすべきバグを切り分けるトリアージ手順を、動くコードとともに整理しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →