Skip to main content
Tappd works seamlessly alongside React Navigation — the most popular navigation library for React Native — to give you automatic screen-level analytics without wiring up tracking calls on every screen manually. You can use a lightweight hook-based approach for individual screens, or set up a centralized navigation tracker that fires on every route change.

Installation

If you haven’t already installed React Navigation, add the core library, the native stack navigator, and the required peer dependencies:
npm install @react-navigation/native @react-navigation/native-stack
npm install react-native-screens react-native-safe-area-context

Basic Setup

Track screens with useFocusEffect

The quickest way to track a screen is to call tappd.trackScreen() inside the useFocusEffect hook. The hook fires every time the screen comes into focus, so you get an accurate view even when users navigate back to it.
import React from 'react';
import { View, Text } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import { useTappd } from '../contexts/TappdContext';

function HomeScreen() {
  const tappd = useTappd();

  useFocusEffect(
    React.useCallback(() => {
      tappd.trackScreen('HomeScreen', {
        category: 'main',
        section: 'dashboard',
      });

      return () => {
        // Optional: run cleanup when the screen loses focus
      };
    }, [])
  );

  return (
    <View>
      <Text>Home Screen</Text>
    </View>
  );
}
For a centralized approach that tracks every screen transition automatically, create a useNavigationTracking helper and attach it to your NavigationContainer.

Create the navigation helper

// utils/navigationTracker.js
import { useEffect, useRef } from 'react';
import { useNavigationContainerRef } from '@react-navigation/native';

export function useNavigationTracking(tappd) {
  const navigationRef = useNavigationContainerRef();
  const routeNameRef = useRef();

  useEffect(() => {
    if (!navigationRef || !tappd) return;

    const onStateChange = async () => {
      const previousRouteName = routeNameRef.current;
      const currentRouteName = navigationRef.getCurrentRoute()?.name;

      if (previousRouteName !== currentRouteName) {
        await tappd.trackScreen(currentRouteName, {
          previousScreen: previousRouteName,
        });
      }

      routeNameRef.current = currentRouteName;
    };

    navigationRef.addListener('state', onStateChange);

    return () => {
      navigationRef.removeListener('state', onStateChange);
    };
  }, [navigationRef, tappd]);

  return navigationRef;
}

Use the helper in App.js

// App.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useTappd } from './contexts/TappdContext';
import { useNavigationTracking } from './utils/navigationTracker';

const Stack = createNativeStackNavigator();

function App() {
  const tappd = useTappd();
  const navigationRef = useNavigationTracking(tappd);

  return (
    <NavigationContainer ref={navigationRef}>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Profile" component={ProfileScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Advanced: Custom Navigator

If you want tracking baked into a reusable navigator component, wrap createNativeStackNavigator in a TrackedStackNavigator that listens for state changes internally.
// components/TrackedStackNavigator.js
import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useNavigationContainerRef } from '@react-navigation/native';
import { useTappd } from '../contexts/TappdContext';

const Stack = createNativeStackNavigator();

export function TrackedStackNavigator({ screens }) {
  const tappd = useTappd();
  const navigationRef = useNavigationContainerRef();
  const routeNameRef = React.useRef();

  React.useEffect(() => {
    if (!navigationRef || !tappd) return;

    const unsubscribe = navigationRef.addListener('state', async () => {
      const previousRouteName = routeNameRef.current;
      const currentRouteName = navigationRef.getCurrentRoute()?.name;

      if (previousRouteName !== currentRouteName) {
        await tappd.trackScreen(currentRouteName, {
          previousScreen: previousRouteName,
          timestamp: new Date().toISOString(),
        });
      }

      routeNameRef.current = currentRouteName;
    });

    return unsubscribe;
  }, [navigationRef, tappd]);

  return (
    <Stack.Navigator>
      {screens.map((screen) => (
        <Stack.Screen
          key={screen.name}
          name={screen.name}
          component={screen.component}
          options={screen.options}
        />
      ))}
    </Stack.Navigator>
  );
}
Use it just like the standard stack navigator:
<TrackedStackNavigator
  screens={[
    { name: 'Home', component: HomeScreen },
    { name: 'Profile', component: ProfileScreen },
  ]}
/>

Tab Navigation

For bottom tab navigators, add useFocusEffect inside each tab screen so tracking fires when a user switches tabs:
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native';

const Tab = createBottomTabNavigator();

function HomeTab() {
  const tappd = useTappd();

  useFocusEffect(
    React.useCallback(() => {
      tappd.trackScreen('HomeTab');
    }, [])
  );

  return <HomeScreen />;
}

function ProfileTab() {
  const tappd = useTappd();

  useFocusEffect(
    React.useCallback(() => {
      tappd.trackScreen('ProfileTab');
    }, [])
  );

  return <ProfileScreen />;
}

function TabNavigator() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="HomeTab" component={HomeTab} />
      <Tab.Screen name="ProfileTab" component={ProfileTab} />
    </Tab.Navigator>
  );
}

Tracking Navigation Events

Track explicit navigation actions — for example, when a user taps a button that takes them to another screen — by calling tappd.track() before calling navigation.navigate():
import { useNavigation } from '@react-navigation/native';

function MyScreen() {
  const navigation = useNavigation();
  const tappd = useTappd();

  const handleNavigate = async (screenName, params) => {
    await tappd.track('navigation.navigated', {
      from: 'CurrentScreen',
      to: screenName,
      params: params,
    });

    navigation.navigate(screenName, params);
  };

  return (
    <Button title="Go to Profile" onPress={() => handleNavigate('Profile')} />
  );
}

Deep Linking

The SDK automatically tracks deep_link.opened events. For additional context — such as distinguishing between cold-start and warm-start deep links — you can supplement automatic tracking with manual calls:
import { Linking } from 'react-native';

function App() {
  const tappd = useTappd();

  useEffect(() => {
    // Cold start: app launched via deep link
    Linking.getInitialURL().then((url) => {
      if (url) {
        tappd.track('deep_link.handled', {
          url: url,
          source: 'cold_start',
        });
      }
    });

    // Warm start: app already running when deep link arrives
    const subscription = Linking.addEventListener('url', (event) => {
      tappd.track('deep_link.handled', {
        url: event.url,
        source: 'warm_start',
      });
    });

    return () => {
      subscription.remove();
    };
  }, []);

  return (
    // Your app
  );
}

Screen Tracking with Params

Pass route parameters as tracking context to enrich your analytics data:
function ProductScreen({ route }) {
  const tappd = useTappd();
  const { productId } = route.params;

  useFocusEffect(
    React.useCallback(() => {
      tappd.trackScreen('ProductScreen', {
        productId: productId,
        category: 'ecommerce',
      });
    }, [productId])
  );

  return (
    // Screen content
  );
}

Complete Example

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import TappdSDK from '@tappd/mobile-sdk';
import { TappdProvider } from './contexts/TappdContext';
import HomeScreen from './screens/HomeScreen';
import ProfileScreen from './screens/ProfileScreen';

const Stack = createNativeStackNavigator();

const tappd = new TappdSDK({
  appId: 'YOUR_APP_ID',
  enableAutoScreenTracking: true,
});

function App() {
  return (
    <TappdProvider value={tappd}>
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen name="Home" component={HomeScreen} />
          <Stack.Screen name="Profile" component={ProfileScreen} />
        </Stack.Navigator>
      </NavigationContainer>
    </TappdProvider>
  );
}

export default App;

API Reference

MethodSignatureReturnsDescription
tappd.trackScreen(screenName: string, properties?: EventProperties)Promise<void>Records a screen view event. Pass properties to attach arbitrary metadata — route params, categories, or any context useful for analysis.
The EventProperties type accepts any flat or nested object of strings, numbers, or booleans.
// Minimal call
await tappd.trackScreen('HomeScreen');

// With properties
await tappd.trackScreen('ProductScreen', {
  productId: 'abc123',
  category: 'electronics',
});

Best Practices

Follow these patterns to get the most reliable screen tracking data across your app:
  • Use useFocusEffect over useEffectuseEffect only fires on mount, while useFocusEffect fires every time the screen gains focus, giving you accurate revisit counts.
  • Always track with context — Pass relevant route params (product IDs, category names, etc.) so your analytics have enough detail to be actionable.
  • Use consistent screen names — Pick a naming convention and stick with it. Inconsistent names (e.g., "home" vs. "HomeScreen" vs. "Home") fragment your funnel data.
  • Track explicit navigation actions — Supplementing automatic screen tracking with event-level tracking (e.g., navigation.button_pressed) gives you a complete picture of user intent.
  • Handle deep links — Even though the SDK auto-tracks deep_link.opened, adding source context (cold_start vs. warm_start) helps attribute acquisition correctly.