Skip to content

Commit 0f70e5d

Browse files
authored
fix(JS, stack): Fix push fail after 3rd screen in JS stack (#3450)
Fixes software-mansion/react-native-screens-labs#583 ## Description This PR changes the logic for creating update transactions in `ScreenContainer.onUpdate()`. In JS stack, the list of screens on stack is repopulated on each update. Some screens can be set to not detach when covered. By some mysterious logic, we've been adding them to the `pendingFront` list and adding them on top. We change this logic so every screen except for the first one are added in the same way. ## Changes Modified & refactored logic for reattaching screens in `ScreenContainer.onUpdate()`. ## Test code and steps to reproduce Use Test3450.
1 parent 1746a58 commit 0f70e5d

File tree

3 files changed

+362
-9
lines changed

3 files changed

+362
-9
lines changed

android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -411,18 +411,27 @@ open class ScreenContainer(
411411
// attach newly activated screens
412412
var addedBefore = false
413413
val pendingFront: ArrayList<ScreenFragmentWrapper> = ArrayList()
414-
415414
for (fragmentWrapper in screenWrappers) {
415+
fragmentWrapper.screen.setTransitioning(transitioning)
416+
416417
val activityState = getActivityState(fragmentWrapper)
417-
if (activityState !== ActivityState.INACTIVE && !fragmentWrapper.fragment.isAdded) {
418-
addedBefore = true
419-
attachScreen(it, fragmentWrapper.fragment)
420-
} else if (activityState !== ActivityState.INACTIVE && addedBefore) {
421-
// we detach the screen and then reattach it later to make it appear on front
422-
detachScreen(it, fragmentWrapper.fragment)
423-
pendingFront.add(fragmentWrapper)
418+
if (activityState == ActivityState.INACTIVE) {
419+
continue
420+
}
421+
422+
if (fragmentWrapper.fragment.isAdded) {
423+
if (addedBefore) {
424+
detachScreen(it, fragmentWrapper.fragment)
425+
pendingFront.add(fragmentWrapper)
426+
}
427+
} else {
428+
if (addedBefore) {
429+
pendingFront.add(fragmentWrapper)
430+
} else {
431+
addedBefore = true
432+
attachScreen(it, fragmentWrapper.fragment)
433+
}
424434
}
425-
fragmentWrapper.screen.setTransitioning(transitioning)
426435
}
427436

428437
for (screenFragment in pendingFront) {

apps/src/tests/Test3450.tsx

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
import * as React from 'react';
2+
import { ScrollView, Text, View, StyleSheet, Pressable } from 'react-native';
3+
import {
4+
createStaticNavigation,
5+
CommonActions,
6+
NavigationIndependentTree,
7+
NavigationContainer,
8+
StackActions,
9+
} from '@react-navigation/native';
10+
import { createStackNavigator, StackAnimationName, StackNavigationOptions } from '@react-navigation/stack';
11+
import { SettingsPicker, SettingsSwitch } from '../shared';
12+
import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack';
13+
import Colors from '../shared/styling/Colors';
14+
import { Button } from '@react-navigation/elements';
15+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
16+
import { SafeAreaView } from 'react-native-safe-area-context';
17+
18+
// #region Types
19+
20+
interface StackScreenConfig {
21+
title: string;
22+
presentation: Exclude<StackNavigationOptions['presentation'], undefined>;
23+
animation: StackAnimationName;
24+
detachPreviousScreen: boolean;
25+
}
26+
27+
interface TabScreenConfig {
28+
title: string;
29+
screens: StackScreenConfig[];
30+
}
31+
32+
interface SettableConfigContext<T> {
33+
config: T;
34+
setConfig: React.Dispatch<React.SetStateAction<T>>;
35+
}
36+
37+
interface SingleItemConfigProps<T> {
38+
config: T;
39+
setItemConfig: (config: T) => void;
40+
}
41+
42+
type TestRoutes = {
43+
Config: undefined;
44+
TestStack: undefined;
45+
TestBottomTabs: undefined;
46+
};
47+
48+
// #region Globals and constants
49+
50+
const STACK_ANIMATION_NAMES: StackAnimationName[] = [
51+
'default',
52+
'fade',
53+
'fade_from_bottom',
54+
'fade_from_right',
55+
'none',
56+
'reveal_from_bottom',
57+
'scale_from_center',
58+
'slide_from_bottom',
59+
'slide_from_right',
60+
'slide_from_left'
61+
];
62+
63+
const DEFAULT_SCREEN: StackScreenConfig = {
64+
title: 'Home',
65+
presentation: 'card',
66+
detachPreviousScreen: true,
67+
animation: 'default',
68+
};
69+
70+
const StackConfigContext = React.createContext<SettableConfigContext<StackScreenConfig[]>>({ config: [], setConfig: () => { } });
71+
const BottomTabsConfigContext = React.createContext<SettableConfigContext<TabScreenConfig[]>>({ config: [], setConfig: () => { } });
72+
73+
// #region Test configuration
74+
75+
function applyConfigChange<T>(configList: T[], index: number, config: T) {
76+
configList = [...configList];
77+
configList[index] = config
78+
return configList;
79+
}
80+
81+
function StackScreenConfigItem(props: SingleItemConfigProps<StackScreenConfig>) {
82+
const { config, setItemConfig } = props;
83+
84+
return (
85+
<View style={styles.configItem}>
86+
<Text style={styles.titleText}>{props.config.title}</Text>
87+
<SettingsSwitch
88+
label='detachPreviousScreen'
89+
value={props.config.detachPreviousScreen}
90+
onValueChange={v => setItemConfig({ ...config, detachPreviousScreen: v })}
91+
/>
92+
<SettingsPicker<StackScreenConfig['presentation']>
93+
label='presentation'
94+
value={config.presentation}
95+
onValueChange={v => setItemConfig({ ...config, presentation: v })}
96+
items={['card', 'modal', 'transparentModal']}
97+
/>
98+
<SettingsPicker<StackAnimationName>
99+
label='animation'
100+
value={config.animation}
101+
onValueChange={v => setItemConfig({ ...config, animation: v })}
102+
items={STACK_ANIMATION_NAMES}
103+
/>
104+
</View>
105+
)
106+
}
107+
108+
function BottomTabsScreenConfigItem(props: SingleItemConfigProps<TabScreenConfig>) {
109+
const { config, setItemConfig } = props;
110+
111+
return (
112+
<View style={styles.configItem}>
113+
<Text style={styles.titleText}>{props.config.title}</Text>
114+
{config.screens.map((stackConfig, i) => (
115+
<StackScreenConfigItem
116+
key={i}
117+
config={stackConfig}
118+
setItemConfig={newConfig => setItemConfig({ ...config, screens: applyConfigChange(config.screens, i, newConfig) })}
119+
/>
120+
))}
121+
<Pressable
122+
style={styles.stackButton}
123+
onPress={() => {
124+
setItemConfig({
125+
...config,
126+
screens: [ ...config.screens, { ...DEFAULT_SCREEN, title: `Screen${config.screens.length}` } ],
127+
})
128+
}}
129+
>
130+
<Text>Add</Text>
131+
</Pressable>
132+
<Pressable
133+
style={styles.stackButton}
134+
onPress={() => {
135+
setItemConfig({
136+
...config,
137+
screens: [ ...config.screens.slice(0, -1) ],
138+
})
139+
}}
140+
>
141+
<Text>Remove</Text>
142+
</Pressable>
143+
</View>
144+
)
145+
}
146+
147+
function Config(props: { navigation: NativeStackNavigationProp<TestRoutes> }) {
148+
const { config: stackConfig, setConfig: setStackConfig } = React.useContext(StackConfigContext);
149+
const { config: bottomTabsConfig, setConfig: setBottomTabsConfig } = React.useContext(BottomTabsConfigContext);
150+
const [ scenario, setScenario ] = React.useState<'stack' | 'tabs'>('stack');
151+
152+
return (
153+
<ScrollView style={styles.configList}>
154+
<SafeAreaView>
155+
<SettingsPicker<'stack' | 'tabs'>
156+
label='Scenario'
157+
value={scenario}
158+
onValueChange={s => setScenario(s)}
159+
items={['stack', 'tabs']}
160+
/>
161+
{scenario === 'stack' && (<>
162+
{stackConfig.map((config, i) => (
163+
<StackScreenConfigItem
164+
key={i}
165+
config={config}
166+
setItemConfig={newConfig => setStackConfig(applyConfigChange(stackConfig, i, newConfig))}
167+
/>
168+
))}
169+
<Pressable style={styles.stackButton} onPress={() => {setStackConfig([...stackConfig, { ...DEFAULT_SCREEN, title: `Screen${stackConfig.length}` }])}}>
170+
<Text>Add</Text>
171+
</Pressable>
172+
<Pressable style={styles.stackButton} onPress={() => setStackConfig([...stackConfig.slice(0, -1)])}>
173+
<Text>Remove</Text>
174+
</Pressable>
175+
<Pressable style={styles.stackButton} onPress={() => props.navigation.navigate('TestStack')}>
176+
<Text>Test</Text>
177+
</Pressable>
178+
</>)}
179+
{scenario === 'tabs' && (<>
180+
{bottomTabsConfig.map((config, i) => (
181+
<BottomTabsScreenConfigItem
182+
key={i}
183+
config={config}
184+
setItemConfig={newConfig => setBottomTabsConfig(applyConfigChange(bottomTabsConfig, i, newConfig))}
185+
/>
186+
))}
187+
<Pressable style={styles.tabsButton} onPress={() => {setBottomTabsConfig([...bottomTabsConfig, { title: `Tab${stackConfig.length}`, screens: [{ ...DEFAULT_SCREEN }] }])}}>
188+
<Text>Add</Text>
189+
</Pressable>
190+
<Pressable style={styles.tabsButton} onPress={() => setBottomTabsConfig([...bottomTabsConfig.slice(0, -1)])}>
191+
<Text>Remove</Text>
192+
</Pressable>
193+
<Pressable style={styles.tabsButton} onPress={() => props.navigation.navigate('TestBottomTabs')}>
194+
<Text>Test</Text>
195+
</Pressable>
196+
</>)}
197+
</SafeAreaView>
198+
</ScrollView>
199+
)
200+
}
201+
202+
// #region Test routes
203+
204+
function TestScreen(props: { nextScreen: string | undefined, isFirst: boolean }) {
205+
return (
206+
<View style={{ padding: 8, gap: 8 }}>
207+
{ props.nextScreen && <Button screen={props.nextScreen}>Go to {props.nextScreen}</Button> }
208+
{ !props.isFirst && <Button action={CommonActions.goBack()}>Go back</Button> }
209+
{ !props.isFirst && <Button action={StackActions.pop(2)}>Pop 2</Button> }
210+
</View>
211+
)
212+
}
213+
214+
function TestStack( props: { configList: StackScreenConfig[] }) {
215+
const JSStack = createStackNavigator({
216+
detachInactiveScreens: true,
217+
screens: Object.fromEntries(props.configList.map((config, i) => [
218+
config.title,
219+
{
220+
screen: () => <TestScreen nextScreen={ props.configList.at(i + 1)?.title } isFirst={i === 0} />,
221+
options: {
222+
presentation: config.presentation,
223+
detachPreviousScreen: config.detachPreviousScreen,
224+
animation: config.animation,
225+
gestureEnabled: true,
226+
},
227+
},
228+
]))
229+
});
230+
231+
const Navigation = createStaticNavigation(JSStack);
232+
233+
return (
234+
<NavigationIndependentTree>
235+
<Navigation />
236+
</NavigationIndependentTree>
237+
)
238+
}
239+
240+
function TestBottomTabs( props: { configList: TabScreenConfig[] }) {
241+
const Tab = createBottomTabNavigator();
242+
243+
return (
244+
<Tab.Navigator detachInactiveScreens={true}>
245+
{props.configList.map(config => (
246+
<Tab.Screen
247+
name={config.title}
248+
options={{ headerShown: false }}
249+
component={() => <TestStack configList={config.screens} />}
250+
/>
251+
))}
252+
</Tab.Navigator>
253+
);
254+
}
255+
256+
export default function App() {
257+
const [stackScreenConfig, setStackScreenConfig] = React.useState<StackScreenConfig[]>([
258+
{ ...DEFAULT_SCREEN }
259+
]);
260+
const [tabScreenConfig, setTabScreenConfig] = React.useState<TabScreenConfig[]>([
261+
{
262+
title: 'HomeTab',
263+
screens: [ { ...DEFAULT_SCREEN } ]
264+
}
265+
]);
266+
267+
const Stack = createNativeStackNavigator<TestRoutes>();
268+
269+
return (
270+
<StackConfigContext.Provider value={{
271+
config: stackScreenConfig,
272+
setConfig: setStackScreenConfig,
273+
}}>
274+
<BottomTabsConfigContext.Provider value={{
275+
config: tabScreenConfig,
276+
setConfig: setTabScreenConfig,
277+
}}>
278+
<NavigationIndependentTree>
279+
<NavigationContainer>
280+
<Stack.Navigator>
281+
<Stack.Screen
282+
name='Config'
283+
component={Config}
284+
/>
285+
<Stack.Screen
286+
name='TestStack'
287+
component={() => <TestStack configList={stackScreenConfig} />}
288+
options={{
289+
headerShown: false,
290+
}}
291+
/>
292+
<Stack.Screen
293+
name='TestBottomTabs'
294+
component={() => <TestBottomTabs configList={tabScreenConfig} />}
295+
options={{
296+
headerShown: false,
297+
}}
298+
/>
299+
</Stack.Navigator>
300+
</NavigationContainer>
301+
</NavigationIndependentTree>
302+
</BottomTabsConfigContext.Provider>
303+
</StackConfigContext.Provider>
304+
)
305+
}
306+
307+
const styles = StyleSheet.create({
308+
configList: {
309+
padding: 8,
310+
},
311+
configItem: {
312+
flex: 1,
313+
borderStyle: 'solid',
314+
borderRadius: 16,
315+
borderColor: Colors.cardBorder,
316+
borderWidth: 1,
317+
margin: 4,
318+
},
319+
titleText: {
320+
fontSize: 18,
321+
textAlign: 'center',
322+
},
323+
stackButton: {
324+
borderRadius: 16,
325+
fontWeight: 'bold',
326+
flex: 1,
327+
alignItems: 'center',
328+
justifyContent: 'center',
329+
backgroundColor: Colors.BlueDark100,
330+
margin: 4,
331+
padding: 8,
332+
},
333+
tabsButton: {
334+
borderRadius: 16,
335+
fontWeight: 'bold',
336+
flex: 1,
337+
alignItems: 'center',
338+
justifyContent: 'center',
339+
backgroundColor: Colors.GreenDark100,
340+
margin: 4,
341+
padding: 8,
342+
}
343+
});

apps/src/tests/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export { default as Test3369 } from './Test3369';
165165
export { default as Test3379 } from './Test3379';
166166
export { default as Test3422 } from './Test3422';
167167
export { default as Test3425 } from './Test3425';
168+
export { default as Test3450 } from './Test3450';
168169
export { default as TestScreenAnimation } from './TestScreenAnimation';
169170
// The following test was meant to demo the "go back" gesture using Reanimated
170171
// but the associated PR in react-navigation is currently put on hold

0 commit comments

Comments
 (0)