Stop Refetching on Every Render: A Practical Guide to useFocusEffect in React Navigation
React Navigation keeps screens mounted after you navigate away, so useEffect fires only once. useFocusEffect scopes side effects to focus events—if you memoize the callback and clean up properly.
03 Jul 2026, 04:33 UTC

The mount trap in React Navigation
When you push a screen in a stack navigator or switch tabs, React Navigation often keeps the previous screen mounted in the background. That means useEffect with an empty dependency array runs once when the screen first mounts—and never again when the user returns. If your screen fetches a list, subscribes to a WebSocket, or starts a timer inside useEffect, those side effects are tied to the component lifecycle, not to what the user actually sees.
What useFocusEffect does differently
useFocusEffect ships with @react-navigation/native in v5 and later. It accepts a callback that runs when the screen gains focus and a cleanup function that runs when the screen blurs (loses focus) or unmounts. Because focus is driven by the navigator, the callback may not run on initial mount if the screen is not the active route. For read-only focus state, useIsFocused is lighter weight; reserve useFocusEffect for side effects that need cleanup.
The useCallback trap
The hook re-runs its callback whenever the callback reference changes. If you pass an inline arrow function, a new reference is created on every render, so the effect tears down and sets up again—potentially firing duplicate network requests. Wrap the callback in useCallback with an empty dependency array (or the minimal set of stable dependencies) so the reference stays identical across unrelated renders.
Worked example: a settings screen
Consider a settings screen that loads user preferences on focus and subscribes to a theme-change event. The pattern below keeps the fetch and subscription scoped to visibility:
import { useFocusEffect } from '@react-navigation/native';
import { useCallback, useState } from 'react';
function SettingsScreen() {
const [prefs, setPrefs] = useState(null);
useFocusEffect(
useCallback(() => {
let isActive = true;
// Load preferences when the screen becomes visible
loadPreferences().then((data) => {
if (isActive) setPrefs(data);
});
// Subscribe to an event that only matters while visible
const subscription = EventBus.addListener('themeChanged', (theme) => {
if (isActive) applyTheme(theme);
});
return () => {
isActive = false;
subscription.remove();
};
}, [])
);
// render UI using prefs
}
Two details matter here. First, the isActive flag prevents a state update if the async fetch resolves after the user has navigated away. Second, the cleanup removes the event listener so it does not leak or fire while the screen is blurred.
Trade-offs and limitations
useFocusEffect is not a universal replacement for useEffect. Focus events can fire before native layout finishes in some containers, so avoid using the callback for measurements that depend on final dimensions. The API is also version-sensitive: React Navigation v4 used navigation.addListener('didFocus') instead, so confirm the installed major version before adopting the hook.
| Hook | Runs on mount? | Runs on refocus? | Cleanup on blur? | Typical use |
|---|---|---|---|---|
useEffect | Yes | No | No (unmount only) | One-time setup independent of navigation |
useFocusEffect | Only if focused at mount | Yes | Yes | Focus-scoped fetches, timers, subscriptions |
useIsFocused | N/A (returns boolean) | N/A | N/A | Conditional rendering, read-only checks |
Verify the behavior in your app
Before shipping, confirm the hook behaves as expected in your navigator. Open package.json and check that @react-navigation/native is v5 or higher. Add a console.log inside the effect and another inside the cleanup, then navigate away and back: you should see the cleanup log on blur and the effect log on refocus. Next, wrap the callback in useCallback with an empty dependency array and trigger an unrelated state update; the effect should not re-run. Finally, repeat the flow on both iOS and Android simulators, because focus timing can vary by navigator and platform.
If you need to revert, removing the hook and restoring a plain useEffect is straightforward—but only do so if the screen genuinely needs mount-scoped behavior rather than visibility-scoped behavior.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.