import React, { useRef, useState, useCallback } from 'react';
import { Text, View, Button, StyleSheet, SafeAreaView } from 'react-native';
import {
NavigationContainer,
useNavigation,
useRoute,
type RouteProp,
} from '@react-navigation/native';
import {
createNativeStackNavigator,
type NativeStackNavigationProp,
} from '@react-navigation/native-stack';
import PagerView from 'react-native-pager-view';
type RootStackParamList = {
Home: undefined;
Modal: { onSubmit: () => void };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
const PAGES = ['Page 0', 'Page 1', 'Page 2'];
const PAGE_COLORS = ['#fde2e2', '#e2fde6', '#e2e8fd'];
function HomeScreen() {
const pagerRef = useRef<PagerView>(null);
// currentPage is purely for display in the UI below — it does not drive the
// pager. The pager is controlled imperatively via pagerRef.setPage().
const [currentPage, setCurrentPage] = useState(0);
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Home'>>();
const advancePager = useCallback(() => {
const next = currentPage + 1;
console.log('[setPage]', next);
pagerRef.current?.setPage(next);
setCurrentPage(next);
}, [currentPage]);
const openModal = useCallback(() => {
navigation.navigate('Modal', { onSubmit: advancePager });
}, [advancePager, navigation]);
return (
<SafeAreaView style={styles.flex}>
<View style={styles.controls}>
<Text style={styles.label}>Last requested page: {currentPage}</Text>
<Button title="Advance pager directly" onPress={advancePager} />
<Button title="Open Modal" onPress={openModal} />
</View>
<PagerView
ref={pagerRef}
style={styles.pager}
onPageSelected={e => {
console.log('[onPageSelected]', e.nativeEvent.position);
}}
>
{PAGES.map((label, i) => (
<View
key={label}
style={[styles.page, { backgroundColor: PAGE_COLORS[i] }]}
>
<Text style={styles.pageLabel}>{label}</Text>
</View>
))}
</PagerView>
</SafeAreaView>
);
}
function ModalScreen() {
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Modal'>>();
const route = useRoute<RouteProp<RootStackParamList, 'Modal'>>();
const submit = useCallback(() => {
// Synchronously: invoke the parent callback (which calls setPage on the
// pager underneath this modal), THEN pop the modal off the stack.
route.params.onSubmit();
navigation.popTo('Home');
}, [navigation, route]);
return (
<SafeAreaView style={styles.flex}>
<View style={styles.controls}>
<Text style={styles.label}>Modal screen</Text>
<Button title="Submit & advance pager" onPress={submit} />
</View>
</SafeAreaView>
);
}
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Modal"
component={ModalScreen}
options={{
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
const styles = StyleSheet.create({
flex: { flex: 1 },
controls: { padding: 16, gap: 8 },
label: { fontSize: 14, color: '#444' },
pager: { flex: 1 },
page: { flex: 1, alignItems: 'center', justifyContent: 'center' },
pageLabel: { fontSize: 32, fontWeight: '600' },
});
Environment
Description
On iOS, pagerRef.setPage(N) animates to the wrong page index when called while the pager's screen is obscured by a presented modal. The visible page advances by N+1 instead of N, while React state correctly reflects N.
This is reproducible with react-native-pager-view@8.0.2. The same code path pre-SwiftUI works correctly. The regression appears to be related to v8's SwiftUI TabView(selection:) + withAnimation implementation racing with the modal-pop animation.
The bug surfaces when setPage is called synchronously inside a callback that runs just before the modal pops. If you defer the setPage until after the modal has fully dismissed, it works. If you use setPageWithoutAnimation instead of setPage, it also works (because no withAnimation transaction).
Reproducible Demo
<PagerView scrollEnabled={false}>with a handful of pages on a Home screen inside a createNativeStackNavigator.options={{ presentation: "modal", animation: "slide_from_bottom" }}.Home, navigate toModalpassing a callback (onSubmit) that callspagerRef.current?.setPage(currentPage + 1)Minimal reproduction code
Screen.Recording.2026-06-09.at.13.11.17.mov