Why Your React Navigation Screen Shows Stale Data (And How useFocusEffect Fixes It)
React Navigation keeps screens mounted after you leave them, so a plain useEffect with an empty dependency array never fires again. useFocusEffect runs side effects on every focus, but only if you wrap the callback in useCallback.
22 Feb 2026, 00:50 UTC

The problem: back navigation doesn’t refresh your screen
You tap a list item, push a detail screen, then hit the back gesture. The list still shows the old data. You check your component and see useEffect(() => { loadItems(); }, []). It ran once when the screen first appeared, and it will never run again unless the component unmounts and remounts.
In React Navigation, stack and bottom tab navigators keep previously visited screens mounted by default. The component is still alive in the tree; it is simply hidden behind the screen on top. Because the mount lifecycle already finished, an empty-dependency useEffect has no reason to fire a second time.
Focus events, not mount events
useFocusEffect replaces the mount-only assumption with navigation focus. It accepts a single callback that runs every time the screen becomes the active one. If you return a function from that callback, React Navigation treats it as teardown and runs it when the screen loses focus.
This makes the hook a natural fit for subscriptions, polling, or refetching: you start work when the user arrives and stop it when they leave. It is also the idiomatic refresh trigger in bottom tab navigators, where screens stay mounted after the first visit and users expect a tab to reload when they switch back to it.
The identity trap: wrap the callback in useCallback
useFocusEffect does not take a dependency array like useEffect. It relies entirely on navigation focus events and the identity of the callback you pass. If you inline the callback, a new function is created on every parent render, and the effect re-runs unnecessarily.
Wrapping the callback in useCallback gives it a stable identity so the effect fires only on focus changes. If the callback reads props or state, include those values in the useCallback dependency list so the effect uses fresh values instead of a stale closure.
Worked example: refetching an order list on focus
Imagine an order-history screen that receives a userId prop. You want to reload orders every time the user navigates back to the screen, and you want to cancel any in-flight request when they navigate away.
import { useFocusEffect } from '@react-navigation/native';
import { useCallback, useState } from 'react';
function OrderListScreen({ userId }) {
const [orders, setOrders] = useState([]);
useFocusEffect(
useCallback(() => {
let cancelled = false;
async function loadOrders() {
const data = await fetchOrders(userId);
if (!cancelled) {
setOrders(data);
}
}
loadOrders();
return () => {
cancelled = true;
};
}, [userId])
);
// render orders...
}
The cancelled flag prevents a late response from updating state after the screen has blurred. The userId dependency in useCallback ensures that if the prop changes, the next focus uses the new value rather than a captured one.
Trade-offs and limitations
Focus-scoped execution solves stale data, but it introduces redundant work. Rapid tab switching or repeated push/pop cycles can fire the callback many times in a short window. Pairing the effect with a cache, a debounce, or a stale-while-revalidate strategy—where you show cached data immediately and refresh in the background—keeps the UI responsive without hammering your API.
Navigator behavior also varies. Standard stacks and bottom tabs preserve mounted screens, but some drawer configurations or custom navigators may unmount screens, which changes when cleanup runs. If your screen depends on precise teardown timing, verify it in the specific navigator you are using.
How to verify it works
Build a minimal two-screen stack. On the first screen, log a timestamp inside useFocusEffect. Navigate to the second screen and back. You should see a new log entry on every return, not just the initial mount.
Next, remove the useCallback wrapper and trigger a parent state update. The log should fire again even though focus did not change, confirming why the wrapper is required. Finally, repeat the test in a bottom tab navigator: switching tabs should trigger the focus effect without remounting the component.
If the log fires only once, your screen is likely unmounting on blur. If it fires on every parent render, the callback identity is unstable. Both symptoms point to navigator configuration or a missing useCallback, not a bug in the hook itself.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.