Answer: Focus events should be emitted after the navigation state update
React Navigation currently dispatches focus/blur events **asynchronously** after the navigation state has been updated. When a navigation action is called synchronously (e.g., navigation.navigate('Screen') directly inside a touch handler), React may batch the state update and the focus listener can fire before the batch is flushed, causing the effect to run more than once.
Confirmed facts
useFocusEffect executes its callback on every focus change, including the initial mount and each time the screen becomes active.- If the focus listener fires before the previous screen’s blur cleanup finishes, the effect can run twice or more for a single navigation.
- Wrapping the callback in
useCallback and returning a cleanup function prevents stale closures but does not stop the extra focus events caused by synchronous navigation.
Likely explanation
Because the navigation dispatcher does not guarantee that the focus event is emitted after the state update when the call is synchronous, React’s batching can cause the focus listener to be invoked in the same tick as the state change. This leads to multiple invocations of useFocusEffect for what appears to be a single screen appearance.
Steps to resolve the issue for this case
- Make the navigation call asynchronous so that the focus event is emitted after the state update:
// Example: wrap in InteractionManager to defer until after the current InteractionManager.runAfterInteractions(() => {
navigation.navigate('TargetScreen');
});
- If you cannot change the call site, guard the effect with a flag that tracks whether the screen has already been processed in the current tick:
const processed = useRef(false);
useFocusEffect(
useCallback(() => {
if (processed.current) return;
processed.current = true;
// your logic here
return () => {
processed.current = false; // cleanup for next appearance
};
}, [])
);
- For data‑fetching that must run exactly once per screen visit, prefer
useEffect with an navigation.isFocused() guard instead of useFocusEffect: useEffect(() => {
if (!navigation.isFocused()) return;
// fetch data once
}, [navigation]);
Missing diagnostic detail
If the synchronous navigation originates from inside a useEffect that runs on mount, the timing differs from a handler attached to a UI event. Knowing the source (event handler vs. effect) would determine whether wrapping in InteractionManager or adding a ref‑guard is sufficient.