Skip to content

Navigation integration

Expo Router and Wire AI screen tracking

Expo Router re-exports useNavigationContainerRef, and the kit's useScreenTracking takes anything with getCurrentRoute and addListener("state"). That is the same object, so the wiring is one hook in the root layout and screen views land in the same event stream as the onboarding funnel, under the same key. Only the route name ever leaves the device. Params, query strings, and anything a user typed are never read into the event, which is what makes this safe to leave on in a health or finance app.

Install

terminal
$
npxexpo install expo-router @react-native-async-storage/async-storage
$
npminstall @wireai/activation

API surface on this page read against @wireai/activation 0.13.5 (npm latest, read 2026-08-04) and expo-router 57.0.9 (npm latest, read 2026-08-02). These are the versions the calls below were checked against, not a claim that this exact file is running in production somewhere.

One hook in the root layout

createScreenTracker holds the last reported screen and drops repeats, so a param change or a re-render does not emit. useScreenTracking builds one tracker per mount, reports the current route immediately, and re-reports on every state event from the ref.

shouldTrack is the escape hatch for a route you do not want in analytics at all. The last-screen memory still advances, so skipping one screen does not make the next one look like a repeat and get swallowed.

app/_layout.tsx
1import AsyncStorage from "@react-native-async-storage/async-storage";
2import { Stack, useNavigationContainerRef } from "expo-router";
3import { useLifecycleEvents } from "@wireai/activation";
4import { useScreenTracking } from "@wireai/activation/analytics";
5
6import { wireConfig } from "@/lib/wire";
7
8export default function RootLayout() {
9  const navigationRef = useNavigationContainerRef();
10
11  // app.first_open once ever, app.session_started on every real open
12  useLifecycleEvents({
13    serverUrl: wireConfig.serverUrl,
14    apiKey: wireConfig.apiKey,
15    appId: wireConfig.appId,
16    storage: AsyncStorage,
17  });
18
19  useScreenTracking(navigationRef, {
20    target: { serverUrl: wireConfig.serverUrl, apiKey: wireConfig.apiKey },
21    shouldTrack: (screen) => screen !== "reset-password",
22  });
23
24  return <Stack screenOptions={{ headerShown: false }} />;
25}

Why the session events matter more than the screen names

useLifecycleEvents is what makes wire.track work anywhere else in the app. It registers the app-open that mints the current session id, and track bails without posting when there is not one. Screen views are the nice-to-have here. The app-open is the thing the review gates, the questionnaire firing rules, and the min-sessions logic all read.

Give it storage and the first-open flag survives a relaunch, so app.first_open stays a once-ever event instead of firing on every cold start.

What breaks

The failure modes when Expo Router and Wire AI share an app. None of them throws, which is why they survive code review.

Never mount useLifecycleEvents and useSessionStart together

Both fire an app-open. Mount both and every session counts twice, which quietly halves every per-session rate you compute. Pick one. useLifecycleEvents is the one that also handles first_open.

Route names collide across groups

The tracker walks to the deepest active route and reports its name. In expo-router that name comes from the file, so two index files under different groups can read the same in your analytics. Rename the files, or filter and relabel with shouldTrack and onScreen.

The subscription only re-runs when the ref identity changes

The effect returns early if addListener is not there yet and lists only the ref in its deps, so a ref that is stable but not ready on the first run leaves tracking off for that mount. Call the hook from a component that renders under the navigator, or gate it on useRootNavigationState.

Params never leave the device, and that is deliberate

Only the route name is read. If you need a param in analytics, put it in an explicit event you send yourself. Do not reach for the params object, because the moment screen tracking can carry arbitrary route state it becomes a privacy review problem.

Questions people actually ask

Does this need React Navigation directly?

No. The kit types the ref structurally and imports no navigation library, so expo-router, bare React Navigation, or your own object with getCurrentRoute and addListener all work.

What happens offline?

Lifecycle events route through the kit's event queue, which persists through the storage you pass, batches, retries, and dequeues on acknowledgement. Screen views go through the direct app-event path.

Can I keep a screen out of analytics entirely?

Return false from shouldTrack for that route. The network emit is skipped and the de-dup memory still advances.

Sources

Wire it into your app

Analytics is free to 299k events a month, forever, on any app. 15 founder seats get the whole engine free, 4 filled.

Written by Malik Chohra. 9 years React Native. Shipped an app at 9M monthly users, and took a consumer app from a 4.3 to a 4.9 App Store rating.

Next: PostHog · RevenueCat