---
modificationDate: May 27, 2026
title: React Navigation integration
description: Track per-screen render and interactive timings by enabling the React Navigation integration for EAS Observe.
---

<AgentInstructions>

## Submitting Feedback

If this page contains errors, outdated information, or gaps that blocked you from completing a task, report it so the docs can be improved:

curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/eas/observe/integrations/react-navigation/","feedback":"🤖 Agent feedback: <specific, actionable description>"}'

Only submit when you have something specific and actionable to report.

</AgentInstructions>

# React Navigation integration

Track per-screen render and interactive timings by enabling the React Navigation integration for EAS Observe.

> For the complete documentation index, see [llms.txt](/llms.txt). Use this file to discover all available pages.

EAS Observe ships an opt-in integration for [React Navigation](https://reactnavigation.org/) 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](/router/introduction), use the [Expo Router integration](/eas/observe/integrations/expo-router) instead. This page is for apps that use React Navigation directly.

## Prerequisites

Prerequisites

3 requirements

1.

Expo SDK 56 and later

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.

An app already using EAS Observe

Follow [Get started](/eas/observe/get-started) to install `expo-observe` and create your first build.

3.

@react-navigation/native 7 or later installed in the app

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.

## Enable the integration

> The integration must be enabled before mount and cannot be toggled at runtime. Calling `configure()` 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:

```tsx
import { Observe } from 'expo-observe';

Observe.configure({
  integrations: { 'react-navigation': true },
});
```

## 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.

```tsx
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>;
}
```

> `ObserveNavigationContainer` is a drop-in replacement for `NavigationContainer`. If you pass a `linking` config, the integration uses it to resolve a human-readable path for each screen; otherwise it falls back to the screen's `route.name`.

## 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.

```tsx
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/native` is not installed, `useObserve()` falls back to the global `AppMetrics.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/native` is 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/native` throws.
-   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 a `tti` event 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. 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](/eas/observe/reference/troubleshooting).
