RORK LABEN
MAX — Rork MaxはReact NativeではなくネイティブSwiftアプリを生成し、Appleエコシステム全体を対象にしますDEVICES — iPhone・iPad・Apple Watch・Apple TV・Vision Pro・iMessageまで、一つの記述から広げられますNATIVE — AR・LiDARスキャン、Metalによる3Dゲーム、ウィジェット、Dynamic Island、オンデバイスCore MLを解放しますSEED — RorkはLeft Lane Capital主導の$15Mシードを調達し、Peak XVやa16z Speedrunも参加しましたPAPERLINE — アプリビルダーPaperlineを買収し、エンジニアリング人材の獲得を続ける姿勢ですMARKET — 月間743,000訪問超。Gartnerは2026年末までに新規アプリの75%がローコード/ノーコードで作られると見込みますMAX — Rork MaxはReact NativeではなくネイティブSwiftアプリを生成し、Appleエコシステム全体を対象にしますDEVICES — iPhone・iPad・Apple Watch・Apple TV・Vision Pro・iMessageまで、一つの記述から広げられますNATIVE — AR・LiDARスキャン、Metalによる3Dゲーム、ウィジェット、Dynamic Island、オンデバイスCore MLを解放しますSEED — RorkはLeft Lane Capital主導の$15Mシードを調達し、Peak XVやa16z Speedrunも参加しましたPAPERLINE — アプリビルダーPaperlineを買収し、エンジニアリング人材の獲得を続ける姿勢ですMARKET — 月間743,000訪問超。Gartnerは2026年末までに新規アプリの75%がローコード/ノーコードで作られると見込みます
記事一覧/開発ツール
開発ツール/2026-07-15中級

開発中は黄色い警告だけだったのに、復帰したら落ちた — ルートパラメータに関数を入れていた話

Rork が生成した画面遷移コードが、ルートパラメータに関数と Date を載せていました。開発中は警告で済むのに、アプリを復帰させると params.onFavorite is not a function で落ちます。原因と、ID だけを渡す形への直し方をまとめました。

Expo Router5React Navigation2トラブルシューティング77状態復元2Rork510

Crashlytics に見慣れないクラッシュが並んでいるのに気づいたのは、壁紙アプリの定例確認をしていた朝でした。

TypeError: params.onFavorite is not a function

手元の実機では一度も出たことがありません。詳細画面は毎日触っています。それでも、前日だけで19件。しかも全て Android で、全て「アプリを開き直した直後」に集中していました。

再現できないクラッシュほど気の重いものはありません。ただ、この件は原因までたどり着いてみると、私自身が見て見ぬふりをしていた黄色い警告がそのまま答えでした。

症状 — 落ちるのは「戻ってきたとき」だけ

整理すると、こういう形です。

場面結果
一覧 → 詳細へ遷移正常
詳細でお気に入りボタンをタップ正常
ホームに戻してすぐ復帰正常
ホームに戻して数十分放置 → 復帰クラッシュ
開発中(Expo Go / dev build)黄色い警告のみ

「数十分放置してから戻る」。ここが鍵でした。

その間に OS がアプリのプロセスを回収しています。ユーザーから見れば、アプリはまだ開いたまま。タスク一覧にも残っています。しかし中身は一度死んでいて、復帰時に OS が「さっきの画面」を復元しようとします。

このとき復元されるのが、保存されていたナビゲーション状態です。

再現条件 — 開発者オプションを1つ入れるだけ

数十分待たずに手元で再現できます。Android の開発者向けオプションを開いて、次を有効にします。

  • アクティビティを保持しない(Don't keep activities)

これで、ホームボタンを押した瞬間にプロセスが破棄されます。復帰させれば即座に同じクラッシュが出ます。私の場合、有効にしてから最初の1回目で再現しました。

iOS でも同じことは起きますが、破棄のタイミングがメモリ状況に左右されるため、手元での再現はやや粘りが必要です。まずは Android で確かめるのが早道でした。

原因 — ナビゲーション状態は文字列に変換されて保存される

Rork が生成した遷移コードは、こういう形でした。

// 生成されたまま使っていたコード
const openDetail = (item: Wallpaper) => {
  router.push({
    pathname: "/wallpaper/detail",
    params: {
      item,                       // オブジェクトごと
      publishedAt: item.publishedAt, // Date オブジェクト
      onFavorite: () => toggleFavorite(item.id), // 関数
    },
  });
};

読みやすく、素直です。詳細画面は params.onFavorite() を呼ぶだけで済みます。実際、開発中はこれで何も困りませんでした。

問題は、ナビゲーション状態が 永続化のために JSON へ変換される ことです。

JSON.stringify は関数を静かに捨てます。Date は文字列になります。つまり復元後の params は、こうなっています。

// 復元後に実際に入っていたもの
{
  item: "[object Object]",     // 文字列化されて壊れている
  publishedAt: "2026-07-01T00:00:00.000Z", // Date ではなく string
  onFavorite: undefined,       // 消えた
}

onFavoriteundefined になり、呼んだ瞬間に落ちます。publishedAt.getTime() を呼んでいる箇所も同様です。

そして開発中に出ていた警告が、これでした。

Non-serializable values were found in the navigation state.
Check: wallpaper/detail > params.onFavorite

React Navigation は最初から教えてくれていたわけです。私はこの警告を「動いているから後で」と流していました。復元まで含めた話だと結びつけられていなかったのが正直なところです。

解決策 — params は ID だけにする

直し方は複数ありますが、優先順位ははっきりしています。

対処1: ルートパラメータには識別子だけを載せる(推奨)

一番地味で、一番効きます。params に入れてよいのは、文字列・数値・真偽値だけと決めてしまいます。

// 遷移側
const openDetail = (item: Wallpaper) => {
  router.push({
    pathname: "/wallpaper/detail",
    params: { id: item.id },   // これだけ
  });
};
// 受け側: app/wallpaper/detail.tsx
import { useLocalSearchParams } from "expo-router";
import { useWallpaperStore } from "@/store/wallpaper";
 
export default function WallpaperDetail() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const item = useWallpaperStore((s) => s.byId[id]);
  const toggleFavorite = useWallpaperStore((s) => s.toggleFavorite);
 
  // 復元直後はストアがまだ空の可能性がある
  if (!item) return <DetailSkeleton />;
 
  return (
    <DetailView
      item={item}
      publishedAt={new Date(item.publishedAt)}
      onFavorite={() => toggleFavorite(id)}
    />
  );
}

ID から本体を引き直す。関数はストア側に置く。この形なら、プロセスが死んで復元されても id という文字列さえ残っていれば画面が再構成できます。

なぜ ID だけが安全かというと、ID は URL に書けるからです。ディープリンクで外から /wallpaper/detail?id=zen-042 と叩かれても成立する形。それが、復元にも耐える形と一致します。ここが腑に落ちてから、判断に迷わなくなりました。

if (!item) return <DetailSkeleton /> の一行は省かないでください。復元直後はストアの再読み込みが間に合わないことがあり、ここを抜くと今度は item.uri で落ちます。私は最初これを忘れ、クラッシュの形が変わっただけで喜びかけました。

対処2: Date は ISO 文字列で渡し、受け側で戻す

どうしても日時を params に載せる必要があるなら、文字列で渡します。

router.push({
  pathname: "/wallpaper/detail",
  params: { id: item.id, publishedAt: item.publishedAt.toISOString() },
});
 
// 受け側
const { publishedAt } = useLocalSearchParams<{ publishedAt: string }>();
const date = new Date(publishedAt);

往路と復路で同じ変換を通るため、復元後も同じ値になります。

対処3: 警告を「見なかったことにできない」ようにする

根本の問題は、警告が黄色いままだったことです。開発中だけ、これを例外に昇格させました。

// app/_layout.tsx
if (__DEV__) {
  const origWarn = console.warn;
  console.warn = (...args: unknown[]) => {
    const msg = String(args[0] ?? "");
    if (msg.includes("Non-serializable values were found")) {
      throw new Error(`[navigation] ${msg}`);
    }
    origWarn(...args);
  };
}

乱暴に見えますが、効果は明確でした。以後、生成コードが同じ形を書いた瞬間に手元で止まります。本番では __DEV__ が false なので影響しません。

予防策 — 生成コードは「復帰」まで通してから受け取る

Rork の生成コードが悪いわけではありません。動く形として素直ですし、開発中は実際に動きます。ただ、生成側は「プロセスが死んだ後に復元される」という状況までは面倒を見てくれません。そこは受け取る側の仕事だと考えています。

私は実機確認の手順に、次の一行を足しました。

  • 「アクティビティを保持しない」を ON にしたまま、主要画面を一巡する

所要はアプリ1本あたり3分ほどです。6本を回しても20分足らず。この確認を入れてから、復帰時のクラッシュは Crashlytics 上で見なくなりました。

個人開発では、手元で動いた時点をゴールにしてしまいがちです。私自身、そこで安心して次の機能に移っていました。けれど本当の初回起動は、ユーザーの端末で、しかもプロセスが一度死んだ後に訪れます。その一瞬を自分の手で作ってから渡す。今はそう決めています。

あわせて、遷移まわりの型を先に縛っておくと生成コードの逸脱に気づきやすくなります。

// 許すのはこれだけ、と型で宣言してしまう
type SerializableParams = Record<string, string | number | boolean>;
 
export const pushDetail = (params: { id: string }) =>
  router.push({ pathname: "/wallpaper/detail", params });

遷移を関数に集約しておけば、生成コードがオブジェクトを直接渡そうとした時点で型エラーになります。

切り分けの目安

似た症状に当たったときの、最初に見る順番です。

症状疑う場所
復帰後だけ is not a functionparams に関数を載せていないか
復帰後だけ getTime is not a functionparams の Date が文字列化されている
復帰後だけ画面が空ID から本体を引き直せているか
復帰後に [object Object]オブジェクトを params に載せている
常に落ちるこれとは別件。復元は無関係

「復帰後だけ」という条件が付くなら、ほぼこの一族です。

次にやること

まず、開発中のコンソールに Non-serializable values were found が出ていないかを確認してみてください。出ているなら、その画面が復帰時に落ちる候補です。

そのうえで、遷移コードを ID だけの形に寄せる。この2つで、私の手元では片付きました。

ナビゲーションの構成そのものを見直すなら Expo Router の設計手順 が土台になります。プロセス復帰まわりの状態については 壁紙グリッドに戻ると一番上に戻ってしまう問題 でも、保存と復元のタイミングを扱っています。

警告を1つ潰すだけの、静かな作業です。それでも、再現できないクラッシュが消えていくのは気持ちのよいものでした。お読みいただきありがとうございました。

シェア

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

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

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

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

関連記事

開発ツール2026-04-27
RorkのAndroidアプリで戻るボタンが効かない — BackHandlerの落とし穴と直し方
Rorkで作ったAndroidアプリで、戻るボタンを押すたびにアプリが終了してしまう・想定外の画面に戻る・モーダルが閉じない。BackHandlerが効かない典型的な原因と、Expo Routerでの正しい実装パターンを実コード付きで解説します。
開発ツール2026-06-17
Rorkで書き出したアプリがApp Store Connectで「処理中」のまま止まる — 原因の切り分けと対処
Rorkで書き出したアプリをApp Store Connectにアップロードしたのに、ビルドが「処理中」のまま何時間も止まり、TestFlightにも審査提出にも現れない。私が実際に詰まったときの切り分け手順と、再発を防ぐ事前チェックを実コード付きで整理します。
開発ツール2026-06-12
Rork 製アプリの『書類とデータ』が数 GB に膨らむときの原因と対処 — expo-image のディスクキャッシュ管理
アプリ本体は小さいのに『書類とデータ』だけが数 GB に膨らむ——壁紙アプリの運用で実際に踏んだ expo-image のディスクキャッシュ肥大を、cachePolicy の使い分け・縮小版配信・世代別クリアの三段構えで解決した記録です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →