◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-07-15中級

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

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

Expo Router6React Navigationトラブルシューティング85状態復元Rork585

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,       // 消えた
}

onFavorite は undefined になり、呼んだ瞬間に落ちます。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-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。
⬡ 開発ツール2026-10-07
日本語の見出しだけ太字にならない — Android で fontWeight が効かないときの切り分けと直し方
Rork で作ったアプリで、iPhone では太字なのに Android では日本語の見出しが細いままになる症状の切り分け記事です。原因の見分け方、fontFamily を重みごとに分ける直し方、確認用コードを載せています。
⬡ 開発ツール2026-09-21
exit code 0 で止まるビルド — ログを黙らせていた設定と、空ファイルを通した存在チェック
eas build --local が exit code 0 but produced no further output で止まる件と、0 バイトのスタブが埋め込まれず dyld で落ちる件。どちらも原因は出力を減らす設定でした。自分のスクリプトを棚卸しする手順まで書き残します。