This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

Expo Router integration

Edit page

Track per-route render and interactive timings by enabling the Expo Router integration for EAS Observe.


EAS Observe ships an opt-in integration for Expo Router that collects per-route metrics tagged with the route pattern (for example, /(tabs)/sessions/[sessionId]). This lets you compare navigation performance by route in the dashboard instead of looking only at app-wide aggregates.

Prerequisites

Prerequisites

3 requirements

1.

Expo SDK 56 or later

The Expo Router integration is available on SDK 56 and later. On earlier SDKs, expo-observe still tracks app-wide metrics, but per-route navigation events are not emitted.

2.

An app already using EAS Observe

Follow Get started to install expo-observe and create your first build.

3.

Expo Router installed in the app

The integration depends on expo-router at runtime. If the package is not installed, the integration becomes a silent no-op.

1

Enable the integration

Call Observe.configure() with the expo-router integration flag at module scope, before any screen mounts:

src/app/_layout.tsx
import { Observe } from 'expo-observe'; Observe.configure({ integrations: { 'expo-router': true }, });

2

Call useObserve() in your screens

Use the useObserve() hook to get a markInteractive that is automatically scoped to the current route. The emitted event is tagged with the screen's route pattern.

src/app/(tabs)/index.tsx
import { useObserve } from 'expo-observe'; import { useEffect } from 'react'; export default function Home() { const { markInteractive } = useObserve(); useEffect(() => { markInteractive(); }, [markInteractive]); return (/* your screen content */); }

Filter sensitive URL parameters

By default, the integration includes the resolved url and all serializable route and query parameters in routeParams. If your app includes sensitive values in URL parameters, pass their keys to filteredParams:

src/app/_layout.tsx
import { Observe } from 'expo-observe'; Observe.configure({ integrations: { 'expo-router': { filteredParams: ['userId', 'token'], }, }, });

The integration removes filtered keys from routeParams, and the event omits url and includes urlHidden: true instead. routeName is not affected because it is a pattern and never contains parameter values.

Metrics

Per-route first render (cold_ttr)

What it measures: Time from when a navigation action is dispatched (for example, a link click) to when the destination screen first becomes focused. For the very first focus after app launch, the measurement is taken from when the JS bundle is loaded, and the event includes isAppLaunch: true.

Emitted at most once per screen instance within a session.

Event params:

ParamTypeDescription
routeNamestringRoute pattern, for example /(tabs)/sessions/[sessionId].
urlstringResolved pathname for the navigation.
urlHiddenbooleanPresent as true when url is omitted because a parameter was filtered.
routeParamsobjectResolved route params (for example, { sessionId: 'abc' }).
isAppLaunchbooleantrue when measured against process start, false for subsequent navigation.

Per-route warm render (warm_ttr)

What it measures: Same as cold_ttr, but for screens that were already rendered before focus, typically because they were preloaded via <Link prefetch /> or the user navigated back to them.

Event params:

ParamTypeDescription
routeNamestringRoute pattern, for example /(tabs)/sessions/[sessionId].
urlstringResolved pathname for the navigation.
urlHiddenbooleanPresent as true when url is omitted because a parameter was filtered.
routeParamsobjectResolved route params (for example, { sessionId: 'abc' }).

Per-route time to interactive (tti)

What it measures: Time from when a navigation action is dispatched to when markInteractive() is called on the destination screen. Only the first call per navigation is recorded, so it is safe to call markInteractive() multiple times.

Event params:

ParamTypeDescription
routeNamestringRoute pattern, for example /(tabs)/sessions/[sessionId].
urlstringResolved pathname.
urlHiddenbooleanPresent as true when url is omitted because a parameter was filtered.
routeParamsobjectResolved route params.
...anyAny custom params passed via markInteractive({ params: { ... } }).

Notes and troubleshooting

  • routeName is a pattern (/(tabs)/sessions/[sessionId]), not a resolved URL (/sessions/abc). This keeps metrics stable across distinct param values so the dashboard buckets them together. Resolved values are still available on the event via url and routeParams.
  • Calls to router.prefetch() do not count as a user navigation and never seed a cold_ttr or warm_ttr measurement. The next user-driven navigation to that route emits warm_ttr because the screen has already rendered.
  • The integration only activates if expo-router is installed at runtime. If it is not installed, useObserve() and ObserveRoot continue to work but no per-route navigation metrics are emitted.
  • The integration must be enabled before mount via Observe.configure({ integrations: { 'expo-router': true } }). Toggling it after the app has mounted throws.
  • If markInteractive() logs Calling markInteractive on unmounted screen or No metadata available for the current screen, the call ran outside a screen component or after unmount. Move the call into a useEffect inside the screen component.
  • For general issues with EAS Observe, see Troubleshooting.