I'm facing a strange issue in a React Native app where an API request sometimes gets triggered twice after the app comes back from the background.
It seems to happen mostly on Android. On the initial screen load everything works normally.
I have a screen where I fetch the latest data when the screen is focused:
const fetchData = async () => {
try {
console.log('API CALLED');
const response = await api.get('/dashboard');
setData(response.data);
} catch (error) {
console.log('API error:', error);
}
};
useFocusEffect(
useCallback(() => {
fetchData();
return () => {
console.log('Screen unfocused');
};
}, [])
);
Normally I get:
API CALLED
only once.
But if I:
Open this screen
Put the app in the background
Wait for a few seconds
Open the app again
I sometimes see:
API CALLED
API CALLED
and two requests appear in the network logs almost at the same time.
I initially thought the component was being mounted twice, so I added:
useEffect(() => {
console.log('MOUNTED');
return () => {
console.log('UNMOUNTED');
};
}, []);
But when the duplicate request happens, I don't always see the component unmounting/remounting.
I also checked that fetchData() isn't being called from another useEffect.
React Native: 0.81.x
React Navigation: 7.x
React: 19.x
Platform: Android
I can prevent the duplicate request with a ref:
const isFetching = useRef(false);
const fetchData = async () => {
if (isFetching.current) return;
isFetching.current = true;
try {
const response = await api.get('/dashboard');
setData(response.data);
} finally {
isFetching.current = false;
}
};
This works as a guard, but I'm not sure if I'm just hiding the actual problem.
Can useFocusEffect run again when an Android app returns from the background even if the screen was never unmounted?
If so, what is the recommended pattern when I want to fetch once when navigating to the screen, but don't want duplicate requests when the app resumes?
This can happen on Android when the app goes between background and foreground. useFocusEffect may run again when the screen regains focus, so if the previous request is still running, you can end up with two requests.
If you want to fetch whenever the screen gets focus, I'd cancel the previous request when the effect is cleaned up:
useFocusEffect(
useCallback(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
console.log('API CALLED');
const response = await api.get('/dashboard', {
signal: controller.signal,
});
setData(response.data);
} catch (error: any) {
if (error.name !== 'CanceledError' && error.name !== 'AbortError') {
console.log('API error:', error);
}
}
};
fetchData();
return () => {
controller.abort();
};
}, [])
);
If you only want the request to run when the component mounts, use a normal useEffect instead:
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
try {
const response = await api.get('/dashboard');
if (isMounted) {
setData(response.data);
}
} catch (error) {
console.log('API error:', error);
}
};
fetchData();
return () => {
isMounted = false;
};
}, []);
For a larger app, I'd probably use something like TanStack Query rather than handling caching, deduplication, and refetching manually.