Skip to content

Commit 4123e4b

Browse files
authored
chore(examples): allow to set route options in StackContainer (#3625)
## Description Add `SetRouteOptionsActionMethod` allowing to modify route options. Closes <software-mansion/react-native-screens-labs#936> It only modifies options of the particular `StackRoute` indicated by the passed `routeKey`. Route configuration is an input to the `StackContainer` & it shouldn't be changed from `StackContainer`. This is sometimes useful however, but it should be introduced separately. ## Changes I've added this to `StackContainer` "navigation methods". It does not fit well to the naming, but I think that code requires a bit of cleanup / refactor anyway. ## Visual documentation https://github.com/user-attachments/assets/babb79f0-f479-4f02-bb5a-5658c46edb4e ## Test plan I've added option to dynamically configure `preventNativeDismiss` in `prevent-native-dismiss-single-stack` & `prevent-native-dismiss-nested-stack`. This has been missing from that tests. ## Checklist - [x] Included code example that can be used to test this change. - [x] Updated / created local changelog entries in relevant test files. - [x] For visual changes, included screenshots / GIFs / recordings documenting the change. - [x] For API changes, updated relevant public types. - [x] Ensured that CI passes
1 parent 718728c commit 4123e4b

File tree

7 files changed

+122
-11
lines changed

7 files changed

+122
-11
lines changed

apps/src/shared/gamma/containers/stack/StackContainer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export function StackContainer({ routeConfigs }: StackContainerProps) {
6969
pop: navMethods.popAction,
7070
preload: navMethods.preloadAction,
7171
batch: navMethods.batchAction,
72+
setRouteOptions: navMethods.setRouteOptions,
7273
};
7374

7475
return (

apps/src/shared/gamma/containers/stack/StackContainer.types.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export type StackRouteOptions = Omit<
88
'children' | 'activityMode' | 'screenKey'
99
>;
1010

11+
/**
12+
* Blueprint for a route.
13+
*/
1114
export type StackRouteConfig = {
1215
name: string;
1316
Component: React.ComponentType;
@@ -31,6 +34,10 @@ export type PopCompletedActionMethod = (routeKey: string) => void;
3134
export type PopNativeActionMethod = (routeKey: string) => void;
3235
export type PreloadActionMethod = (routeName: string) => void;
3336
export type BatchActionMethod = (actions: BatchableNavigationAction[]) => void;
37+
export type SetRouteOptionsActionMethod = (
38+
routeKey: string,
39+
options: Partial<StackRouteOptions>,
40+
) => void;
3441
export type ClearEffectsActionMethod = () => void;
3542

3643
export type NavigationActionMethods = {
@@ -41,6 +48,12 @@ export type NavigationActionMethods = {
4148
preloadAction: PreloadActionMethod;
4249
batchAction: BatchActionMethod;
4350
clearEffectsAction: ClearEffectsActionMethod;
51+
/**
52+
* Change options for the current route. This does not modify a blueprint
53+
* (StackRouteConfig) for the route. It only modifies options for the
54+
* given route instance.
55+
*/
56+
setRouteOptions: SetRouteOptionsActionMethod;
4457
};
4558

4659
export type StackState = StackRoute[];
@@ -98,14 +111,22 @@ export type NavigationActionClearEffects = {
98111
ctx: NavigationActionContext;
99112
};
100113

114+
export type NavigationActionSetRouteOptions = {
115+
type: 'set-options';
116+
routeKey: string;
117+
options: Partial<StackRouteOptions>;
118+
ctx: NavigationActionContext;
119+
};
120+
101121
export type NavigationActionContext = {
102122
routeConfigs: StackRouteConfig[];
103123
};
104124

105125
export type BatchableNavigationAction =
106126
| Omit<NavigationActionPush, 'ctx'>
107127
| Omit<NavigationActionPop, 'ctx'>
108-
| Omit<NavigationActionPreload, 'ctx'>;
128+
| Omit<NavigationActionPreload, 'ctx'>
129+
| Omit<NavigationActionSetRouteOptions, 'ctx'>;
109130

110131
export type NavigationAction =
111132
| NavigationActionPush
@@ -114,4 +135,5 @@ export type NavigationAction =
114135
| NavigationActionNativePop
115136
| NavigationActionPreload
116137
| NavigationActionClearEffects
138+
| NavigationActionSetRouteOptions
117139
| NavigationActionBatch;

apps/src/shared/gamma/containers/stack/contexts/StackNavigationContext.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
PopActionMethod,
55
PreloadActionMethod,
66
PushActionMethod,
7+
SetRouteOptionsActionMethod,
78
StackRouteOptions,
89
} from '../StackContainer.types';
910

@@ -14,6 +15,7 @@ export type StackNavigationContextPayload = {
1415
pop: PopActionMethod;
1516
preload: PreloadActionMethod;
1617
batch: BatchActionMethod;
18+
setRouteOptions: SetRouteOptionsActionMethod;
1719
};
1820

1921
export const StackNavigationContext =

apps/src/shared/gamma/containers/stack/hooks/useStackOperationMethods.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {
1111
PopNativeActionMethod,
1212
PreloadActionMethod,
1313
PushActionMethod,
14+
SetRouteOptionsActionMethod as SetRouteOptionsActionMethod,
1415
StackRouteConfig,
16+
StackRouteOptions,
1517
} from '../StackContainer.types';
1618

1719
export function useStackOperationMethods(
@@ -79,6 +81,13 @@ export function useStackOperationMethods(
7981
dispatch({ type: 'clear-effects', ctx: actionContext });
8082
}, [dispatch, actionContext]);
8183

84+
const setRouteOptions: SetRouteOptionsActionMethod = React.useCallback(
85+
(routeKey: string, options: Partial<StackRouteOptions>) => {
86+
dispatch({ type: 'set-options', routeKey, options, ctx: actionContext });
87+
},
88+
[dispatch, actionContext],
89+
);
90+
8291
const aggregateValue = React.useMemo(() => {
8392
return {
8493
pushAction,
@@ -88,6 +97,7 @@ export function useStackOperationMethods(
8897
preloadAction,
8998
batchAction,
9099
clearEffectsAction,
100+
setRouteOptions,
91101
};
92102
}, [
93103
pushAction,
@@ -97,6 +107,7 @@ export function useStackOperationMethods(
97107
preloadAction,
98108
batchAction,
99109
clearEffectsAction,
110+
setRouteOptions,
100111
]);
101112

102113
return aggregateValue;

apps/src/shared/gamma/containers/stack/reducer.tsx

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
NavigationActionPopCompleted,
99
NavigationActionPreload,
1010
NavigationActionPush,
11+
NavigationActionSetRouteOptions,
1112
StackNavigationEffect,
1213
StackNavigationState,
1314
StackRoute,
@@ -44,6 +45,9 @@ export function navigationStateReducer(
4445
case 'clear-effects': {
4546
return navigationActionClearEffectsHandler(state, action);
4647
}
48+
case 'set-options': {
49+
return navigationActionSetOptionsHandler(state, action);
50+
}
4751
}
4852

4953
// @ts-ignore
@@ -87,7 +91,7 @@ function navigationActionPushHandler(
8791
const newStack = stack.toSpliced(renderedRouteIndex, 1);
8892
const routeCopy = { ...route };
8993
routeCopy.activityMode = 'attached';
90-
return stackNavStateWithStack(state, applyPush(newStack, routeCopy));
94+
return stateWithStack(state, applyPush(newStack, routeCopy));
9195
}
9296

9397
// 2 - Try to render new route
@@ -101,7 +105,7 @@ function navigationActionPushHandler(
101105
}
102106

103107
const newRoute = createRouteFromConfig(newRouteConfig, 'attached');
104-
return stackNavStateWithStack(state, applyPush(state.stack, newRoute));
108+
return stateWithStack(state, applyPush(state.stack, newRoute));
105109
}
106110

107111
function navigationActionPopHandler(
@@ -132,7 +136,7 @@ function navigationActionPopHandler(
132136
// If there is already a pop-container effect, do nothing
133137
return state;
134138
}
135-
return stackNavStateWithEffects(
139+
return stateWithEffects(
136140
state,
137141
applyEffect(state.effects, { type: 'pop-container' }),
138142
);
@@ -164,7 +168,7 @@ function navigationActionPopHandler(
164168
// and the original state won't be immediatelly affected.
165169
route.activityMode = 'detached';
166170

167-
return stackNavStateWithStack(state, newStack);
171+
return stateWithStack(state, newStack);
168172
}
169173

170174
function navigationActionPopCompletedHandler(
@@ -192,7 +196,7 @@ function navigationActionPopCompletedHandler(
192196
// Let's remove the route from the state
193197
// TODO: Consider adding option for keeping it in state.
194198
const newStack = stack.toSpliced(routeIndex, 1);
195-
return stackNavStateWithStack(state, newStack);
199+
return stateWithStack(state, newStack);
196200
}
197201

198202
function navigationActionNativePopHandler(
@@ -222,7 +226,7 @@ function navigationActionNativePopHandler(
222226
}
223227

224228
const newStack = stack.toSpliced(routeIndex, 1);
225-
return stackNavStateWithStack(state, newStack);
229+
return stateWithStack(state, newStack);
226230
}
227231

228232
function navigationActionPreloadHandler(
@@ -244,7 +248,7 @@ function navigationActionPreloadHandler(
244248
// that won't result in problems on native platform.
245249
// More info: https://github.com/software-mansion/react-native-screens/pull/3531.
246250
const newStack = [...state.stack, createRouteFromConfig(routeConfig)];
247-
return stackNavStateWithStack(state, newStack);
251+
return stateWithStack(state, newStack);
248252
}
249253

250254
function navigationActionBatchHandler(
@@ -272,7 +276,33 @@ function navigationActionClearEffectsHandler(
272276
if (state.effects.length === 0) {
273277
return state;
274278
}
275-
return stackNavStateWithEffects(state, []);
279+
return stateWithEffects(state, []);
280+
}
281+
282+
function navigationActionSetOptionsHandler(
283+
state: StackNavigationState,
284+
action: NavigationActionSetRouteOptions,
285+
): StackNavigationState {
286+
const routeIndex = state.stack.findIndex(
287+
route => route.routeKey === action.routeKey,
288+
);
289+
290+
if (routeIndex === NOT_FOUND_INDEX) {
291+
throw new Error(
292+
`[Stack] Can not set options. Route with key ${action.routeKey} not found`,
293+
);
294+
}
295+
296+
const routeCopy = { ...state.stack[routeIndex] };
297+
routeCopy.options = {
298+
...routeCopy.options,
299+
...action.options,
300+
};
301+
302+
return stateWithStack(
303+
state,
304+
stackWithReplacedRoute(state.stack, routeCopy, routeIndex),
305+
);
276306
}
277307

278308
// Ensures correct order of screens (attached first, detached at the end).
@@ -312,7 +342,7 @@ function generateRouteKeyForRouteName(routeName: string): string {
312342
return `r-${routeName}-${generateID()}`;
313343
}
314344

315-
function stackNavStateWithStack(
345+
function stateWithStack(
316346
navState: StackNavigationState,
317347
newStack: StackState,
318348
): StackNavigationState {
@@ -322,7 +352,7 @@ function stackNavStateWithStack(
322352
};
323353
}
324354

325-
function stackNavStateWithEffects(
355+
function stateWithEffects(
326356
navState: StackNavigationState,
327357
newEffects: StackNavigationEffect[],
328358
): StackNavigationState {
@@ -331,3 +361,11 @@ function stackNavStateWithEffects(
331361
effects: newEffects,
332362
};
333363
}
364+
365+
function stackWithReplacedRoute(
366+
state: StackState,
367+
newRoute: StackRoute,
368+
routeIndex: number,
369+
): StackState {
370+
return state.toSpliced(routeIndex, 1, newRoute);
371+
}

apps/src/tests/single-feature-tests/stack-v5/prevent-native-dismiss-nested-stack.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ function AScreen() {
8989
<RouteInformation routeName="A" />
9090
<PreventNativeDismissInfo />
9191
<NavigationButtons isPopEnabled routeNames={['A', 'B', 'NestedStack']} />
92+
<TogglePreventNativeDismiss />
9293
</CenteredLayoutView>
9394
);
9495
}
@@ -99,6 +100,7 @@ function BScreen() {
99100
<RouteInformation routeName="B" />
100101
<PreventNativeDismissInfo />
101102
<NavigationButtons isPopEnabled routeNames={['A', 'B', 'NestedStack']} />
103+
<TogglePreventNativeDismiss />
102104
</CenteredLayoutView>
103105
);
104106
}
@@ -154,6 +156,7 @@ function NestedHomeScreen() {
154156
<RouteInformation routeName="NestedHome" />
155157
<PreventNativeDismissInfo />
156158
<NavigationButtons isPopEnabled routeNames={['NestedA', 'NestedB']} />
159+
<TogglePreventNativeDismiss />
157160
</CenteredLayoutView>
158161
);
159162
}
@@ -164,6 +167,7 @@ function NestedAScreen() {
164167
<RouteInformation routeName="NestedA" />
165168
<PreventNativeDismissInfo />
166169
<NavigationButtons isPopEnabled routeNames={['NestedA', 'NestedB']} />
170+
<TogglePreventNativeDismiss />
167171
</CenteredLayoutView>
168172
);
169173
}
@@ -174,6 +178,7 @@ function NestedBScreen() {
174178
<RouteInformation routeName="NestedB" />
175179
<PreventNativeDismissInfo />
176180
<NavigationButtons isPopEnabled routeNames={['NestedA', 'NestedB']} />
181+
<TogglePreventNativeDismiss />
177182
</CenteredLayoutView>
178183
);
179184
}
@@ -214,6 +219,21 @@ function NavigationButtons(props: {
214219
);
215220
}
216221

222+
function TogglePreventNativeDismiss() {
223+
const navigation = useStackNavigationContext();
224+
225+
return (
226+
<Button
227+
title="Toggle Prevent Native Dismiss"
228+
onPress={() =>
229+
navigation.setRouteOptions(navigation.routeKey, {
230+
preventNativeDismiss: !navigation.routeOptions.preventNativeDismiss,
231+
})
232+
}
233+
/>
234+
);
235+
}
236+
217237
function PreventNativeDismissInfo() {
218238
const navContext = useStackNavigationContext();
219239

apps/src/tests/single-feature-tests/stack-v5/prevent-native-dismiss-single-stack.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ function AScreen() {
7878
<RouteInformation routeName="A" />
7979
<PreventNativeDismissInfo />
8080
<NavigationButtons isPopEnabled={true} />
81+
<TogglePreventNativeDismiss />
8182
</CenteredLayoutView>
8283
);
8384
}
@@ -88,6 +89,7 @@ function BScreen() {
8889
<RouteInformation routeName="B" />
8990
<PreventNativeDismissInfo />
9091
<NavigationButtons isPopEnabled={true} />
92+
<TogglePreventNativeDismiss />
9193
</CenteredLayoutView>
9294
);
9395
}
@@ -120,6 +122,21 @@ function NavigationButtons(props: { isPopEnabled: boolean }) {
120122
);
121123
}
122124

125+
function TogglePreventNativeDismiss() {
126+
const navigation = useStackNavigationContext();
127+
128+
return (
129+
<Button
130+
title="Toggle Prevent Native Dismiss"
131+
onPress={() =>
132+
navigation.setRouteOptions(navigation.routeKey, {
133+
preventNativeDismiss: !navigation.routeOptions.preventNativeDismiss,
134+
})
135+
}
136+
/>
137+
);
138+
}
139+
123140
function PreventNativeDismissInfo() {
124141
const navContext = useStackNavigationContext();
125142

0 commit comments

Comments
 (0)