---
modificationDate: July 17, 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 you encounter errors, misleading or outdated information, report it so Expo can be improved:

Preferred command:
npx --yes submit-expo-feedback@latest --category docs --subject "/eas/observe/integrations/react-navigation/" "<actionable feedback>"

Direct HTTP fallback:
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 for docs: <specific, actionable description> (<model>, <harness>)"}'

Only submit when you have something specific and actionable to report. Try to give the most context.

## Navigation

When answering a related or follow-up question, fetch the relevant page below as Markdown (.md) instead of guessing; use llms.txt for the full map.

You are here: EAS > EAS Observe > Integrations
Pages in this section:
- [Expo Router](https://docs.expo.dev/eas/observe/integrations/expo-router.md)
- [React Navigation](https://docs.expo.dev/eas/observe/integrations/react-navigation.md) (this page)
- [Third-party](https://docs.expo.dev/eas/observe/integrations/third-party.md)
Full documentation tree: [llms.txt](https://docs.expo.dev/llms.txt)

</AgentInstructions>

This documentation is available as Markdown for AI agents and LLMs. See the [full Markdown index](/llms.txt) or append .md to any documentation URL.

# React Navigation integration

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

EAS Observe ships an opt-in integration for [React Navigation](https://reactnavigation.org/) that collects per-screen metrics tagged with the screen's route-name path (for example, `/Tabs/Sessions`). 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.md), use the [Expo Router integration](/eas/observe/integrations/expo-router.md) instead. This page is for apps that use React Navigation directly.

## Prerequisites

#### Prerequisites

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

##### An app already using EAS Observe

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

##### @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 },
});
```

## Connect the integration to your navigation

How you connect the integration depends on whether your app uses React Navigation's [dynamic or static configuration](https://reactnavigation.org/docs/static-vs-dynamic/). Both approaches record the same per-screen metrics.

#### Dynamic

For [dynamic configuration](https://reactnavigation.org/docs/hello-react-navigation/), 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>;
}
```

#### Static

With [static configuration](https://reactnavigation.org/docs/static-configuration/), there is no `NavigationContainer` to replace. `createStaticNavigation()` renders one for you. Instead, create the navigation ref yourself, pass it to the returned `<Navigation>` element, and wrap the element in `<ObserveNavigationProvider>` with the same ref. The provider listens to navigation events through the ref and records the same per-screen render timings.

```tsx
import { createStaticNavigation, useNavigationContainerRef } from '@react-navigation/native';
import { Observe } from 'expo-observe';
import { ObserveNavigationProvider } from 'expo-observe/integrations/react-navigation';

import { RootStack } from './navigation';

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

const Navigation = createStaticNavigation(RootStack);

export default function App() {
  const navigationRef = useNavigationContainerRef();

  return (
    <ObserveNavigationProvider navigationRef={navigationRef}>
      <Navigation ref={navigationRef} />
    </ObserveNavigationProvider>
  );
}
```

> `ObserveNavigationProvider` does not render a container of its own. It listens to the `navigationRef` you pass. It must be an ancestor of every screen so that `useObserve()` works inside them.

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

## Filter sensitive route parameters

> Available in **SDK 57 and later**.

By default, the integration includes all serializable focused route parameters in `routeParams`. If your app includes sensitive values in route parameters, pass their keys to `filteredParams`:

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

Observe.configure({
  integrations: {
    'react-navigation': {
      filteredParams: ['userId', 'token'],
    },
  },
});
```

The integration removes filtered keys from `routeParams` and includes `urlHidden: true` on the event. `routeName` is not affected because it is built from route names and never contains parameter values.

## 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` | Route-name path (for example, `/Tabs/Sessions`). |
| `urlHidden` | `boolean` | Present as `true` when a route parameter was filtered. |
| `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` | Route-name path (for example, `/Tabs/Sessions`). |
| `urlHidden` | `boolean` | Present as `true` when a route parameter was filtered. |
| `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` | Route-name path (for example, `/Tabs/Sessions`). |
| `urlHidden` | `boolean` | Present as `true` when a route parameter was filtered. |
| `routeParams` | `object` | Focused route params. |
| `. .` | `any` | Any custom params passed via `markInteractive({ . . })`. |

## Notes and troubleshooting

-   `routeName` is built from route names (`/Tabs/Sessions`), so route params never appear in the path. This keeps metrics stable across distinct param values so the dashboard buckets them together. Param values are still available on the event via `routeParams`.
-   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>` or `<ObserveNavigationProvider>` 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>` or `<ObserveNavigationProvider>` has mounted, throws.
-   With static configuration, pass the same ref to both `<ObserveNavigationProvider>` and the `<Navigation>` element. If the provider receives a ref that is not connected to a navigation container, no per-screen metrics are emitted.
-   `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.md).
