• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ryanhefner / react-fathom / e61b5ace-542a-4708-b4ff-2e866ef7d8e6

07 Jan 2026 08:52PM UTC coverage: 93.197% (-3.1%) from 96.266%
e61b5ace-542a-4708-b4ff-2e866ef7d8e6

push

circleci

web-flow
Merge pull request #3 from ryanhefner/claude/improve-react-fathom-docs-Cve3K

[feat] React Native Client - Part 1

175 of 193 branches covered (90.67%)

Branch coverage included in aggregate %.

224 of 243 new or added lines in 7 files covered. (92.18%)

373 of 395 relevant lines covered (94.43%)

16.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.12
/src/native/useNavigationTracking.ts
1
import { useEffect, useRef, useCallback } from 'react'
2

3
import { useFathom } from '../hooks/useFathom'
4
import type { UseNavigationTrackingOptions } from './types'
5

6
/**
7
 * Hook that tracks screen navigation as pageviews using React Navigation.
8
 *
9
 * This integrates with React Navigation's navigation container to automatically
10
 * track screen changes as Fathom pageviews.
11
 *
12
 * @example
13
 * ```tsx
14
 * import { NavigationContainer } from '@react-navigation/native'
15
 * import { useNavigationTracking } from 'react-fathom/native'
16
 *
17
 * function App() {
18
 *   const navigationRef = useNavigationContainerRef()
19
 *
20
 *   useNavigationTracking({
21
 *     navigationRef,
22
 *     transformRouteName: (name) => `/screens/${name}`,
23
 *   })
24
 *
25
 *   return (
26
 *     <NavigationContainer ref={navigationRef}>
27
 *       <Navigator />
28
 *     </NavigationContainer>
29
 *   )
30
 * }
31
 * ```
32
 */
33
export function useNavigationTracking(options: UseNavigationTrackingOptions) {
34
  const {
35
    navigationRef,
36
    transformRouteName,
37
    shouldTrackRoute,
38
    includeParams = false,
17✔
39
  } = options
17✔
40

41
  const { trackPageview } = useFathom()
17✔
42
  const routeNameRef = useRef<string | undefined>(undefined)
17✔
43

44
  /**
45
   * Get the current route name from the navigation state
46
   */
47
  const getCurrentRouteName = useCallback((): string | undefined => {
17✔
48
    if (!navigationRef.current) {
19✔
49
      return undefined
1✔
50
    }
51

52
    const state = navigationRef.current.getRootState?.()
18✔
53
    if (!state) {
19✔
54
      return undefined
1✔
55
    }
56

57
    // Navigate through nested navigators to get the deepest route
58
    let currentState = state
17✔
59
    while (currentState.routes[currentState.index]?.state) {
17✔
60
      currentState = currentState.routes[currentState.index].state as any
2✔
61
    }
62

63
    return currentState.routes[currentState.index]?.name
17✔
64
  }, [navigationRef])
65

66
  /**
67
   * Get the current route params from the navigation state
68
   */
69
  const getCurrentRouteParams = useCallback((): Record<string, any> | undefined => {
17✔
70
    if (!navigationRef.current) {
6!
NEW
71
      return undefined
×
72
    }
73

74
    const state = navigationRef.current.getRootState?.()
6✔
75
    if (!state) {
6!
NEW
76
      return undefined
×
77
    }
78

79
    // Navigate through nested navigators to get the deepest route
80
    let currentState = state
6✔
81
    while (currentState.routes[currentState.index]?.state) {
6✔
NEW
82
      currentState = currentState.routes[currentState.index].state as any
×
83
    }
84

85
    return currentState.routes[currentState.index]?.params as Record<string, any> | undefined
6✔
86
  }, [navigationRef])
87

88
  /**
89
   * Build the URL to track
90
   */
91
  const buildTrackingUrl = useCallback(
17✔
92
    (routeName: string, params?: Record<string, any>): string => {
93
      let url = transformRouteName ? transformRouteName(routeName) : `/${routeName}`
16✔
94

95
      if (includeParams && params && Object.keys(params).length > 0) {
16✔
96
        const queryString = Object.entries(params)
6✔
97
          .filter(([_, value]) => value !== undefined && value !== null)
9✔
98
          .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
7✔
99
          .join('&')
100

101
        if (queryString) {
6!
102
          url += `?${queryString}`
6✔
103
        }
104
      }
105

106
      return url
16✔
107
    },
108
    [transformRouteName, includeParams],
109
  )
110

111
  /**
112
   * Handle navigation state change
113
   */
114
  const handleStateChange = useCallback(() => {
17✔
115
    const currentRouteName = getCurrentRouteName()
2✔
116
    const previousRouteName = routeNameRef.current
2✔
117

118
    if (currentRouteName && currentRouteName !== previousRouteName) {
2✔
119
      const params = includeParams ? getCurrentRouteParams() : undefined
1!
120

121
      // Check if this route should be tracked
122
      if (shouldTrackRoute && !shouldTrackRoute(currentRouteName, params)) {
1!
NEW
123
        routeNameRef.current = currentRouteName
×
NEW
124
        return
×
125
      }
126

127
      const url = buildTrackingUrl(currentRouteName, params)
1✔
128

129
      trackPageview?.({
1✔
130
        url,
131
        referrer: previousRouteName ? buildTrackingUrl(previousRouteName) : undefined,
1!
132
      })
133

134
      routeNameRef.current = currentRouteName
1✔
135
    }
136
  }, [
137
    getCurrentRouteName,
138
    getCurrentRouteParams,
139
    shouldTrackRoute,
140
    buildTrackingUrl,
141
    trackPageview,
142
    includeParams,
143
  ])
144

145
  // Track initial route on mount
146
  useEffect(() => {
17✔
147
    // Small delay to ensure navigation is ready
148
    const timeout = setTimeout(() => {
17✔
149
      const initialRoute = getCurrentRouteName()
17✔
150
      if (initialRoute) {
17✔
151
        const params = includeParams ? getCurrentRouteParams() : undefined
15✔
152

153
        if (!shouldTrackRoute || shouldTrackRoute(initialRoute, params)) {
15✔
154
          const url = buildTrackingUrl(initialRoute, params)
14✔
155
          trackPageview?.({ url })
14✔
156
          routeNameRef.current = initialRoute
14✔
157
        }
158
      }
159
    }, 0)
160

161
    return () => clearTimeout(timeout)
17✔
162
  }, []) // Only on mount
163

164
  // Set up navigation state change listener
165
  useEffect(() => {
17✔
166
    if (!navigationRef.current) {
17✔
167
      return
1✔
168
    }
169

170
    // Listen for navigation state changes
171
    const unsubscribe = navigationRef.current.addListener?.('state', handleStateChange)
16✔
172

173
    return () => {
17✔
174
      unsubscribe?.()
16✔
175
    }
176
  }, [navigationRef, handleStateChange])
177
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc