Skip to content

Commit

Permalink
feat: add retaining of the screen to the stack navigator (#11765)
Browse files Browse the repository at this point in the history
The changes are pretty straightforward and should be expressive enough
to understand by reading the codebase. This PR:
- Adds the new retaining feature including changes in the router and
minor changes in the card stack (as temporarily scene can include both
the preloaded route and the one getting detached )
- Moves around some leftovers from the previous PR (#11758 )
- Adds tests 

To test, try the example.
  • Loading branch information
osdnk committed Dec 28, 2023
1 parent 968840c commit 7fe82f7
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 38 deletions.
7 changes: 5 additions & 2 deletions example/src/Screens/StackPreloadFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const DetailsScreen = ({
>
Go to Profile
</Button>
<Button onPress={navigation.retain} style={styles.button}>
Retain
</Button>
</View>
);
};
Expand All @@ -65,7 +68,7 @@ const ProfileScreen = ({
const HomeScreen = ({
navigation,
}: StackScreenProps<PreloadStackParams, 'Home'>) => {
const { navigate, preload, removePreload } = navigation;
const { navigate, preload, remove } = navigation;

return (
<View style={styles.content}>
Expand All @@ -78,7 +81,7 @@ const HomeScreen = ({
<Button onPress={() => navigate('Details')} style={styles.button}>
Navigate Details
</Button>
<Button onPress={() => removePreload('Details')} style={styles.button}>
<Button onPress={() => remove('Details')} style={styles.button}>
Remove Details preload
</Button>
</View>
Expand Down
9 changes: 0 additions & 9 deletions packages/routers/src/CommonActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,6 @@ export type Action =
};
source?: string;
target?: string;
}
| {
type: 'REMOVE_PRELOAD';
payload: {
name: string;
params?: object;
};
source?: string;
target?: string;
};

export function goBack(): Action {
Expand Down
48 changes: 43 additions & 5 deletions packages/routers/src/StackRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { nanoid } from 'nanoid/non-secure';

import { BaseRouter } from './BaseRouter';
import type { Action } from './CommonActions';
import type {
CommonNavigationAction,
DefaultRouterOptions,
Expand Down Expand Up @@ -45,6 +44,20 @@ export type StackActionType =
};
source?: string;
target?: string;
}
| {
type: 'REMOVE';
payload: {
name: string;
params?: object;
};
source?: string;
target?: string;
}
| {
type: 'RETAIN';
source?: string;
target?: string;
};

export type StackRouterOptions = DefaultRouterOptions;
Expand Down Expand Up @@ -126,7 +139,7 @@ export type StackActionHelpers<ParamList extends ParamListBase> = {
* @param name Name of the route to remove preload.
* @param [params] Params object for the route.
*/
removePreload<RouteName extends keyof ParamList>(
remove<RouteName extends keyof ParamList>(
...args: RouteName extends unknown
? undefined extends ParamList[RouteName]
?
Expand All @@ -135,6 +148,13 @@ export type StackActionHelpers<ParamList extends ParamListBase> = {
: [screen: RouteName, params: ParamList[RouteName]]
: never
): void;

/**
* Removes a screen from the active routes, at the same time
* retaining the screen in the preloaded screens list,
* so it is not getting detached.
*/
retain(): void;
};

export const StackActions = {
Expand All @@ -153,8 +173,11 @@ export const StackActions = {
popTo(name: string, params?: object, merge?: boolean): StackActionType {
return { type: 'POP_TO', payload: { name, params, merge } };
},
removePreload(name: string, params?: object): Action {
return { type: 'REMOVE_PRELOAD', payload: { name, params } };
remove(name: string, params?: object): StackActionType {
return { type: 'REMOVE', payload: { name, params } };
},
retain(): StackActionType {
return { type: 'RETAIN' };
},
};

Expand Down Expand Up @@ -724,7 +747,22 @@ export function StackRouter(options: StackRouterOptions) {
};
}
}
case 'REMOVE_PRELOAD': {
case 'RETAIN': {
const index =
action.target === state.key && action.source
? state.routes.findIndex((r) => r.key === action.source)
: state.index;
const route = state.routes[index];

return {
...state,
index: state.index - 1,
routes: state.routes.filter((r) => r !== route),
preloadedRoutes: state.preloadedRoutes.concat(route),
};
}

case 'REMOVE': {
const getId = options.routeGetIdList[action.payload.name];
const id = getId?.({ params: action.payload.params });

Expand Down
17 changes: 0 additions & 17 deletions packages/routers/src/TabRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -449,23 +449,6 @@ export function TabRouter({
};
}

case 'REMOVE_PRELOAD': {
const routeIndex = state.routes.findIndex(
(route) => route.name === action.payload.name
);
if (routeIndex === -1) {
return null;
}
const route = state.routes[routeIndex];

return {
...state,
preloadedRouteKeys: state.preloadedRouteKeys.filter(
(key) => key !== route.key
),
};
}

default:
return BaseRouter.getStateForAction(state, action);
}
Expand Down
43 changes: 41 additions & 2 deletions packages/routers/src/__tests__/StackRouter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2159,7 +2159,7 @@ it('handles screen preloading', () => {
],
},

StackActions.removePreload('bar', { answer: 43 }),
StackActions.remove('bar', { answer: 43 }),
options
)
).toEqual({
Expand Down Expand Up @@ -2405,7 +2405,7 @@ it('handles screen preloading', () => {
],
},

StackActions.removePreload('bar', { answer: 42 }),
StackActions.remove('bar', { answer: 42 }),
options
)
).toEqual({
Expand All @@ -2417,4 +2417,43 @@ it('handles screen preloading', () => {
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'qux-test', name: 'qux' }],
});

expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
preloadedRoutes: [],
routeNames: ['baz', 'bar', 'qux'],
routes: [
{
key: 'baz-test',
name: 'baz',
},
{
key: 'qux-test',
name: 'qux',
},
],
},

StackActions.retain(),
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 0,
preloadedRoutes: [{ key: 'qux-test', name: 'qux' }],
routeNames: ['baz', 'bar', 'qux'],
routes: [
{
key: 'baz-test',
name: 'baz',
},
],
});
});
4 changes: 2 additions & 2 deletions packages/stack/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ it('handles screens preloading', async () => {
expect(
queryByText('Screen B', { includeHiddenElements: true })
).not.toBeNull();
act(() => navigation.dispatch(StackActions.removePreload('B')));
act(() => navigation.dispatch(StackActions.remove('B')));
expect(queryByText('Screen B', { includeHiddenElements: true })).toBeNull();
});

Expand Down Expand Up @@ -172,7 +172,7 @@ it('runs focus effect on focus change on preloaded route', () => {
expect(focusEffectCleanup).not.toHaveBeenCalled();

act(() => navigation.preload('A'));
act(() => navigation.dispatch(StackActions.removePreload('B')));
act(() => navigation.dispatch(StackActions.remove('B')));
act(() => navigation.preload('B'));

expect(focusEffect).not.toHaveBeenCalled();
Expand Down
14 changes: 13 additions & 1 deletion packages/stack/src/views/Stack/CardStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,19 @@ export class CardStack extends React.Component<Props, State> {
const focused = focusedRoute.key === route.key;
const gesture = gestures[route.key];
const scene = scenes[index];
const isPreloaded = state.preloadedRoutes.includes(route);
// It is possible that for a short period the route appears in both arrays.
// Particularly, if the screen is removed with `retain`, then it needs a moment to execute the animation.
// However, due to the router action, it immediately populates the `preloadedRoutes` array.
// Practically, the logic below takes care that it is rendered only once.
const isPreloaded =
state.preloadedRoutes.includes(route) && !routes.includes(route);
if (
state.preloadedRoutes.includes(route) &&
routes.includes(route) &&
index >= routes.length
) {
return null;
}

// For the screens that shouldn't be active, the value is 0
// For those that should be active, but are not the top screen, the value is 1
Expand Down

0 comments on commit 7fe82f7

Please sign in to comment.