◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-07-15Intermediate

A Yellow Warning in Dev, a Crash on Resume — Functions in Route Params

Rork generated navigation code that put a function and a Date into route params. In development it was only a warning. After the OS reclaimed the process, resuming the app crashed with params.onFavorite is not a function. Here is the cause and the fix.

Expo Router6React NavigationTroubleshooting43State RestorationRork585

An unfamiliar crash was sitting at the top of Crashlytics during my usual morning check on the wallpaper apps.

TypeError: params.onFavorite is not a function

I had never seen it on my own device. I open that detail screen every day. Still, 19 occurrences the previous day — all Android, all clustered in the moments right after someone reopened the app.

A crash you cannot reproduce is a heavy thing to carry around. Once I traced this one, though, the answer turned out to be a yellow warning I had been politely ignoring for weeks.

The symptom — it only breaks on the way back

Laid out plainly:

SituationResult
List → detail navigationFine
Tapping the favorite button on detailFine
Background the app, return immediatelyFine
Background the app for 30+ minutes, then returnCrash
Development (Expo Go / dev build)Warning only

That fourth row is the whole story.

While the app sits in the background, the OS reclaims the process. To the user, nothing has happened — the app is still there in the task switcher. But the app died, and on resume the OS tries to put the user back on the screen they left.

What gets rebuilt at that moment is the saved navigation state.

Reproducing it — one developer option

You do not have to wait 30 minutes. Open the Android developer options and turn on:

  • Don't keep activities

Now the process is destroyed the instant you press Home. Return to the app and the crash appears immediately. Mine reproduced on the very first try.

iOS behaves the same way in principle, but the timing depends on memory pressure, so reproducing it by hand takes patience. Android was the faster route.

The cause — navigation state is serialized to JSON

Here is what Rork generated, more or less untouched:

// The generated code, as I had been shipping it
const openDetail = (item: Wallpaper) => {
  router.push({
    pathname: "/wallpaper/detail",
    params: {
      item,                          // the whole object
      publishedAt: item.publishedAt, // a Date instance
      onFavorite: () => toggleFavorite(item.id), // a function
    },
  });
};

It reads well. The detail screen just calls params.onFavorite(). Nothing about it caused trouble in development.

The catch is that navigation state gets converted to JSON so it can be persisted.

JSON.stringify drops functions silently. Dates become strings. So after restoration, params actually held this:

// What was really there after resume
{
  item: "[object Object]",                 // stringified into nothing useful
  publishedAt: "2026-07-01T00:00:00.000Z", // a string, not a Date
  onFavorite: undefined,                   // gone
}

onFavorite is undefined, and calling it throws. Anything doing publishedAt.getTime() breaks the same way.

And the warning I had been seeing all along:

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

React Navigation had been telling me from day one. I had filed it under "it works, I'll look later." I had simply never connected it to state restoration.

The fix — put nothing but identifiers in params

There are several ways through this, but the ordering is not ambiguous.

Fix 1: only identifiers in route params (recommended)

The dullest option, and the one that actually works. Decide that params may hold strings, numbers, and booleans — nothing else.

// Navigating
const openDetail = (item: Wallpaper) => {
  router.push({
    pathname: "/wallpaper/detail",
    params: { id: item.id },   // that's all
  });
};
// Receiving: 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);
 
  // Right after restoration the store may not be hydrated yet
  if (!item) return <DetailSkeleton />;
 
  return (
    <DetailView
      item={item}
      publishedAt={new Date(item.publishedAt)}
      onFavorite={() => toggleFavorite(id)}
    />
  );
}

Look the object back up from the ID. Keep the callback in the store. Now, however violently the process dies, one surviving string is enough to rebuild the screen.

The reason identifiers are safe is that an identifier can live in a URL. If a deep link hits /wallpaper/detail?id=zen-042 from outside, the screen still works. That shape and the restoration-proof shape are the same shape. Once that clicked, the decision stopped being a judgment call.

Do not skip the if (!item) return <DetailSkeleton /> line. Store hydration can lag a restore, and without that guard you simply move the crash to item.uri. I forgot it on my first pass and briefly celebrated a crash that had merely changed its name.

Fix 2: pass Dates as ISO strings and rebuild them

If a timestamp genuinely has to travel in params, send a string.

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

Both the outbound and the restored path go through the same conversion, so the value survives.

Fix 3: make the warning impossible to ignore

The root problem was that the warning stayed yellow. In development, I promoted it to an exception.

// 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);
  };
}

Blunt, but decisive. From then on, the moment generated code reaches for that pattern, it stops on my machine. Production is untouched because __DEV__ is false there.

Prevention — run generated code through a resume before accepting it

None of this is Rork's fault. The generated code is a reasonable, readable way to express the intent, and it does work while you are building. What the generator cannot know is that the process will die and the screen will be rebuilt from a JSON blob. Handling that is the receiving side's job, and I prefer to treat it that way.

I added one line to my device checklist:

  • Walk the main screens with "Don't keep activities" left on

About 3 minutes per app. Under 20 minutes across all six. Since adding it, resume crashes have stopped showing up in Crashlytics.

Pinning down the types at the navigation boundary helps too — deviations in generated code surface immediately.

// Declare what's allowed, and let the compiler hold the line
type SerializableParams = Record<string, string | number | boolean>;
 
export const pushDetail = (params: { id: string }) =>
  router.push({ pathname: "/wallpaper/detail", params });

Funnel navigation through functions like this, and the moment generated code tries to hand over an object, you get a type error instead of a Crashlytics entry.

A triage table

When something similar turns up, this is the order I check:

SymptomWhere to look
is not a function, only on resumeA function is riding in params
getTime is not a function, only on resumeA Date in params became a string
Blank screen, only on resumeYou are not re-fetching by ID
[object Object] after resumeAn object is riding in params
Crashes every timeDifferent problem — restoration is not involved

If the words "only on resume" fit, it is almost certainly this family.

What to do next

Start by checking your development console for Non-serializable values were found. Every screen it names is a candidate for crashing on resume.

Then reshape those navigation calls to carry an ID and nothing more. Those two steps closed the case for me.

If you are reworking the navigation structure itself, the Expo Router design walkthrough is a good foundation. For state that has to survive a process death, the piece on wallpaper grids jumping back to the top covers the save-and-restore timing in more depth.

Quiet work — clearing one warning. But watching an unreproducible crash disappear was a good feeling.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.
⬡ Dev Tools2026-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.
⬡ Dev Tools2026-06-17
Your Rork App Is Stuck on "Processing" in App Store Connect — A Field Guide to Getting It Unstuck
You uploaded a Rork-built app to App Store Connect, but the build sits on Processing for hours and never shows up in TestFlight or for submission. Here is the exact diagnostic order I follow, how to read the ITMS error codes, and a short script that reads the processing state from the API — with working code.