Expo Router integration
Edit page
Track per-route render and interactive timings by enabling the Expo Router integration for Expo Observe.
For the complete documentation index, see llms.txt. Use this file to discover all available pages.
Expo 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
3 requirements
3 requirements
1.
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.
Follow Get started to install expo-observe and create your first
build.
3.
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
The integration must be enabled before mount and cannot be toggled at runtime. Callingconfigure()after the app has mounted, or toggling the flag mid-session, throws an error.
Call ExpoObserve.configure() with the expo-router integration flag at module scope, before any screen mounts:
import ExpoObserve from 'expo-observe'; ExpoObserve.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.
import { useObserve } from 'expo-observe'; import { useEffect } from 'react'; export default function Home() { const { markInteractive } = useObserve(); useEffect(() => { markInteractive(); }, [markInteractive]); return (/* your screen content */); }
If the integration is disabled orexpo-routeris not installed,useObserve()falls back to the globalAppMetrics.markInteractive. You can leave the hook in place regardless of integration state.
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:
| Param | Type | Description |
|---|---|---|
routeName | string | Route pattern, for example /(tabs)/sessions/[sessionId]. |
url | string | Resolved pathname for the navigation. |
routeParams | object | Resolved route params (for example, { sessionId: 'abc' }). |
isAppLaunch | boolean | true 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:
| Param | Type | Description |
|---|---|---|
routeName | string | Route pattern, for example /(tabs)/sessions/[sessionId]. |
url | string | Resolved pathname for the navigation. |
routeParams | object | Resolved 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:
| Param | Type | Description |
|---|---|---|
routeName | string | Route pattern, for example /(tabs)/sessions/[sessionId]. |
url | string | Resolved pathname. |
routeParams | object | Resolved route params. |
... | any | Any custom params passed via markInteractive({ params: { ... } }). |
Notes and troubleshooting
routeNameis 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 viaurlandrouteParams.- Calls to
router.prefetch()do not count as a user navigation and never seed acold_ttrorwarm_ttrmeasurement. The next user-driven navigation to that route emitswarm_ttrbecause the screen has already rendered. - The integration only activates if
expo-routeris installed at runtime. If it is not installed,useObserve()andObserveRootcontinue to work but no per-route navigation metrics are emitted. - The integration must be enabled before mount via
ExpoObserve.configure({ integrations: { 'expo-router': true } }). Toggling it after the app has mounted throws. - If
markInteractive()logsCalling markInteractive on unmounted screenorNo metadata available for the current screen, the call ran outside a screen component or after unmount. Move the call into auseEffectinside the screen component. - For general issues with Expo Observe, see Troubleshooting.