Navigation listeners added via navigation.addListener() that are never unsubscribed accumulate across screen mounts. Each time the screen re-enters the stack — through forward navigation, tab switches, or deep links — another listener stacks on top of the previous ones. The result is duplicate event firings: a focus handler that fires once on first mount fires twice after the second mount, three times after the third. This causes duplicate API calls, doubled analytics events, and in apps with optimistic state updates, race conditions that corrupt UI state. CWE-401 (Memory Leak) and ISO 25010:2011 reliability both cover this class of resource leak.
Low because listener leaks cause duplicate event firing and memory accumulation rather than data exposure or crashes, but they compound with every navigation cycle and degrade reliability over a session.
Return the unsubscribe function from useEffect or useFocusEffect for every addListener call. Prefer useFocusEffect from React Navigation, which handles cleanup automatically:
import { useFocusEffect } from '@react-navigation/native'
import { useNavigation } from '@react-navigation/native'
function HomeScreen() {
const navigation = useNavigation()
useFocusEffect(
React.useCallback(() => {
const unsubFocus = navigation.addListener('focus', handleFocus)
const unsubBlur = navigation.addListener('blur', handleBlur)
// cleanup runs on screen blur and unmount
return () => {
unsubFocus()
unsubBlur()
}
}, [navigation])
)
}
If you use useEffect instead, the cleanup function is equally required:
useEffect(() => {
const unsub = navigation.addListener('beforeRemove', handleBeforeRemove)
return unsub // addListener returns the unsubscribe function directly
}, [navigation])
Audit every addListener call in the codebase and confirm each one has a corresponding cleanup path — report the count even when all are clean.
ID: mobile-navigation-linking.state-management.listener-cleanup
Severity: low
What to look for: Search for uses of addListener from useNavigation() hook or navigation prop. Check whether the unsubscribe function returned by addListener is properly called in a cleanup function (return value of useEffect).
Pass criteria: Enumerate all addListener calls and their corresponding cleanup functions. All navigation event listeners (focus, blur, beforeRemove, etc.) must be unsubscribed in useEffect cleanup. No more than 0 dangling listeners should remain after unmount. Report even on pass: "Found X navigation listeners, all with cleanup functions."
Fail criteria: Listeners are added but never cleaned up, or cleanup is incomplete. This can cause memory leaks and duplicate event handling.
Skip (N/A) when: The app does not use navigation event listeners (0 addListener calls found).
Detail on fail: "Navigation.addListener used for focus events but cleanup function is missing" or "Multiple focus event handlers are triggered after navigation changes (indicates no cleanup)"
Remediation: Always cleanup navigation listeners:
function HomeScreen() {
const navigation = useNavigation()
useFocusEffect(
useCallback(() => {
const unsubscribeFocus = navigation.addListener('focus', () => {
console.log('Screen focused')
})
const unsubscribeBlur = navigation.addListener('blur', () => {
console.log('Screen blurred')
})
return () => {
unsubscribeFocus()
unsubscribeBlur()
}
}, [navigation])
)
return <View>{/* content */}</View>
}
Using useFocusEffect from React Navigation handles cleanup automatically and is preferred over manual useEffect with addListener.