React Navigation integration
Edit page
Track per-screen render and interactive timings by enabling the React Navigation integration for EAS Observe.
For the complete documentation index, see llms.txt. Use this file to discover all available pages.
EAS Observe ships an opt-in integration for React Navigation that collects per-screen metrics tagged with the screen's path (for example, /sessions/abc). This lets you compare navigation performance by screen in the dashboard instead of looking only at app-wide aggregates.
If your app uses Expo Router, use the Expo Router integration instead. This page is for apps that use React Navigation directly.
Prerequisites
3 requirements
3 requirements
1.
The React Navigation integration is available on SDK 56 and later. On earlier SDKs,
expo-observe still tracks app-wide metrics, but per-screen navigation events are not emitted.
2.
Follow Get started to install expo-observe and create your first
build.
3.
The integration depends on @react-navigation/native (v7.0.0 or later) 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 Observe.configure() with the react-navigation integration flag at module scope, before any screen mounts:
import { Observe } from 'expo-observe'; Observe.configure({ integrations: { 'react-navigation': true }, });
2
Use ObserveNavigationContainer
Replace your top-level <NavigationContainer> with <ObserveNavigationContainer>. It wraps the stock container, accepts the same props, and forwards the same ref. It also subscribes to navigation state changes so it can record per-screen render timings.
import { Observe } from 'expo-observe'; import { ObserveNavigationContainer } from 'expo-observe/integrations/react-navigation'; Observe.configure({ integrations: { 'react-navigation': true }, }); export default function App() { return <ObserveNavigationContainer>{/* your navigators */}</ObserveNavigationContainer>; }
ObserveNavigationContaineris a drop-in replacement forNavigationContainer. If you pass alinkingconfig, the integration uses it to resolve a human-readable path for each screen; otherwise it falls back to the screen'sroute.name.
3
Call useObserve() in your screens
Use the useObserve() hook to get a markInteractive that is automatically scoped to the current screen. The emitted event is tagged with the screen's path.
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 or@react-navigation/nativeis not installed,useObserve()falls back to the globalAppMetrics.markInteractive. You can leave the hook in place regardless of integration state.
Metrics
Per-screen first render (cold_ttr)
What it measures: Time from when a navigation action is dispatched (for example, navigation.navigate()) 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 | Resolved screen path (for example /sessions/abc), or route.name if no linking config resolves it. |
routeParams | object | Focused route params (for example, { sessionId: 'abc' }). |
isAppLaunch | boolean | true when measured against process start, false for subsequent navigation. |
Per-screen 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 or the user navigated back to them. Tab-navigator siblings count as warm only once they have been mounted. With React Navigation v7's default lazy: true, unfocused tabs stay unmounted and their first focus is recorded as cold_ttr.
Event params:
| Param | Type | Description |
|---|---|---|
routeName | string | Resolved screen path, or route.name. |
routeParams | object | Focused route params (for example, { sessionId: 'abc' }). |
Per-screen 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 | Resolved screen path, or route.name. |
routeParams | object | Focused route params. |
... | any | Any custom params passed via markInteractive({ ... }). |
Notes and troubleshooting
- The integration only activates if
@react-navigation/nativeis installed at runtime. If it is not installed,useObserve()continues to work, but no per-screen navigation metrics are emitted. Rendering<ObserveNavigationContainer>without@react-navigation/nativethrows. - The integration must be enabled before mount via
Observe.configure({ integrations: { 'react-navigation': true } }). Toggling it after the app has mounted, or after<ObserveNavigationContainer>has mounted, throws. markInteractive()only records once the screen is focused. Calls made on an unfocused screen update internal state but do not emit attievent until the screen is focused.- Call
useObserve()inside the screen component, not in a higher-level wrapper. If the screen's identity changes between renders, the hook logs a warning. IfmarkInteractive()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 EAS Observe, see Troubleshooting.