useFocusEffect fires incorrectly after synchronous navigation actions
0 reputation · 18 Mar 2020, 21:48 UTC
0 reputation · 18 Mar 2020, 21:48 UTC
Goal: Clarify whether React Navigation should emit focus (focus/blur) events synchronously before the navigation state update or asynchronously after the transition completes, so developers can predict when useFocusEffect callbacks run in relation to state changes.
Uncertainty: The library has not settled on a timing policy, and when navigation actions are dispatched synchronously (e.g., navigation.navigate called directly from an event handler), the current implementation may fire focus events either before or after the state mutation, leading to useEffect running multiple times or with stale dependencies.
Specific questions: Should focus events be emitted before the state update to match the visual focus change? If emitted after, how can useEffect safely depend on the latest route parameters? What pattern ensures useFocusEffect runs exactly once per screen appearance regardless of dispatch timing?
34775 reputation · 19 Mar 2020, 05:14 UTC
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.
useFocusEffect executes its callback on every focus change, including the initial mount and each time the screen becomes active.useCallback and returning a cleanup function prevents stale closures but does not stop the extra focus events caused by synchronous navigation.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.
// Example: wrap in InteractionManager to defer until after the current InteractionManager.runAfterInteractions(() => {
navigation.navigate('TargetScreen');
});const processed = useRef(false);
useFocusEffect(
useCallback(() => {
if (processed.current) return;
processed.current = true;
// your logic here
return () => {
processed.current = false; // cleanup for next appearance
};
}, [])
);useEffect with an navigation.isFocused() guard instead of useFocusEffect:useEffect(() => {
if (!navigation.isFocused()) return;
// fetch data once
}, [navigation]);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.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 18 Mar 2020, 23:43 UTC
In React Navigation v6 the emitFocus function is called synchronously during the navigation state update, so the focus listener runs before the component re‑renders and receives the newest route.params. However, when navigation.navigate is invoked directly from an event handler, React may batch that state update with other pending updates. If the batch is flushed before the focus listener is processed, the listener can fire in the same tick as the state change, causing useFocusEffect to run twice for a single screen appearance. Wrapping the navigation call in InteractionManager.runAfterInteractions or a zero‑timeout setTimeout forces the focus event to be emitted after the batch flush, restoring the expected single execution. This explains why the workaround works without needing extra flags in the effect.