@react-navigation/routers 8.0.0-alpha.2 → 8.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/lib/module/BaseRouter.js +64 -61
  2. package/lib/module/BaseRouter.js.map +1 -1
  3. package/lib/module/CommonActions.js +9 -21
  4. package/lib/module/CommonActions.js.map +1 -1
  5. package/lib/module/DrawerRouter.js +15 -14
  6. package/lib/module/DrawerRouter.js.map +1 -1
  7. package/lib/module/StackRouter.js +287 -166
  8. package/lib/module/StackRouter.js.map +1 -1
  9. package/lib/module/SwitchRouter.js +426 -0
  10. package/lib/module/SwitchRouter.js.map +1 -0
  11. package/lib/module/TabRouter.js +14 -290
  12. package/lib/module/TabRouter.js.map +1 -1
  13. package/lib/typescript/src/BaseRouter.d.ts +10 -8
  14. package/lib/typescript/src/BaseRouter.d.ts.map +1 -1
  15. package/lib/typescript/src/CommonActions.d.ts +42 -39
  16. package/lib/typescript/src/CommonActions.d.ts.map +1 -1
  17. package/lib/typescript/src/DrawerRouter.d.ts +6 -5
  18. package/lib/typescript/src/DrawerRouter.d.ts.map +1 -1
  19. package/lib/typescript/src/StackRouter.d.ts +70 -24
  20. package/lib/typescript/src/StackRouter.d.ts.map +1 -1
  21. package/lib/typescript/src/SwitchRouter.d.ts +69 -0
  22. package/lib/typescript/src/SwitchRouter.d.ts.map +1 -0
  23. package/lib/typescript/src/TabRouter.d.ts +4 -24
  24. package/lib/typescript/src/TabRouter.d.ts.map +1 -1
  25. package/lib/typescript/src/createParamsFromAction.d.ts +2 -2
  26. package/lib/typescript/src/createParamsFromAction.d.ts.map +1 -1
  27. package/lib/typescript/src/createRouteFromAction.d.ts +2 -2
  28. package/lib/typescript/src/createRouteFromAction.d.ts.map +1 -1
  29. package/lib/typescript/src/index.d.ts +9 -9
  30. package/lib/typescript/src/index.d.ts.map +1 -1
  31. package/lib/typescript/src/types.d.ts +20 -30
  32. package/lib/typescript/src/types.d.ts.map +1 -1
  33. package/package.json +8 -8
  34. package/src/BaseRouter.tsx +93 -80
  35. package/src/CommonActions.tsx +44 -66
  36. package/src/DrawerRouter.tsx +28 -22
  37. package/src/StackRouter.tsx +551 -238
  38. package/src/SwitchRouter.tsx +724 -0
  39. package/src/TabRouter.tsx +24 -463
  40. package/src/createParamsFromAction.tsx +1 -1
  41. package/src/createRouteFromAction.tsx +1 -1
  42. package/src/types.tsx +39 -41
@@ -6,7 +6,6 @@ import { createRouteFromAction } from './createRouteFromAction';
6
6
  import type {
7
7
  CommonNavigationAction,
8
8
  DefaultRouterOptions,
9
- NavigationRoute,
10
9
  NavigationState,
11
10
  ParamListBase,
12
11
  Route,
@@ -16,36 +15,48 @@ import type {
16
15
  export type StackActionType =
17
16
  | {
18
17
  type: 'REPLACE';
19
- payload: { name: string; params?: object };
20
- source?: string;
21
- target?: string;
18
+ payload: { name: string; params?: object | undefined };
19
+ source?: string | undefined;
20
+ target?: string | undefined;
22
21
  }
23
22
  | {
24
23
  type: 'PUSH';
25
- payload: { name: string; params?: object };
26
- source?: string;
27
- target?: string;
24
+ payload: { name: string; params?: object | undefined };
25
+ source?: string | undefined;
26
+ target?: string | undefined;
28
27
  }
29
28
  | {
30
29
  type: 'POP';
31
30
  payload: { count: number };
32
- source?: string;
33
- target?: string;
31
+ source?: string | undefined;
32
+ target?: string | undefined;
34
33
  }
35
34
  | {
36
35
  type: 'POP_TO_TOP';
37
- source?: string;
38
- target?: string;
36
+ source?: string | undefined;
37
+ target?: string | undefined;
39
38
  }
40
39
  | {
41
40
  type: 'POP_TO';
42
41
  payload: {
43
42
  name: string;
44
- params?: object;
45
- merge?: boolean;
43
+ params?: object | undefined;
44
+ merge?: boolean | undefined;
46
45
  };
47
- source?: string;
48
- target?: string;
46
+ source?: string | undefined;
47
+ target?: string | undefined;
48
+ }
49
+ | {
50
+ type: 'RETAIN';
51
+ payload: { enable: boolean };
52
+ source?: string | undefined;
53
+ target?: string | undefined;
54
+ }
55
+ | {
56
+ type: 'DISMISS';
57
+ payload: { count: number };
58
+ source?: string | undefined;
59
+ target?: string | undefined;
49
60
  };
50
61
 
51
62
  export type StackRouterOptions = DefaultRouterOptions;
@@ -57,14 +68,16 @@ export type StackNavigationState<ParamList extends ParamListBase> =
57
68
  */
58
69
  type: 'stack';
59
70
  /**
60
- * List of routes, which are supposed to be preloaded before navigating to.
71
+ * List of route keys to retain in the stack.
72
+ * When a retained route gets removed from the stack,
73
+ * it'll be kept in routes and can be navigated to again.
61
74
  */
62
- preloadedRoutes: NavigationRoute<ParamList, keyof ParamList>[];
75
+ retainedRouteKeys: string[];
63
76
  };
64
77
 
65
78
  export type StackActionHelpers<ParamList extends ParamListBase> = {
66
79
  /**
67
- * Replace the current route with a new one.
80
+ * Replace the current screen with a new one.
68
81
  *
69
82
  * @param screen Name of the new route that will replace the current one.
70
83
  * @param [params] Params object for the new route.
@@ -92,12 +105,15 @@ export type StackActionHelpers<ParamList extends ParamListBase> = {
92
105
  ): void;
93
106
 
94
107
  /**
95
- * Pop a screen from the stack.
108
+ * Pop a history entry from the stack.
109
+ * Also dismisses any screens above the current one.
110
+ *
111
+ * @param [count=1] Number of history entries to pop. Defaults to 1.
96
112
  */
97
113
  pop(count?: number): void;
98
114
 
99
115
  /**
100
- * Pop to the first route in the stack, dismissing all other screens.
116
+ * Pop to the first screen in the stack, dismissing all other screens.
101
117
  */
102
118
  popToTop(): void;
103
119
 
@@ -115,15 +131,32 @@ export type StackActionHelpers<ParamList extends ParamListBase> = {
115
131
  ? [
116
132
  screen: RouteName,
117
133
  params?: ParamList[RouteName],
118
- options?: { merge?: boolean },
134
+ options?: { merge?: boolean | undefined },
119
135
  ]
120
136
  : [
121
137
  screen: RouteName,
122
138
  params: ParamList[RouteName],
123
- options?: { merge?: boolean },
139
+ options?: { merge?: boolean | undefined },
124
140
  ]
125
141
  : never
126
142
  ): void;
143
+
144
+ /**
145
+ * Enable or disable retaining the current screen in the stack.
146
+ *
147
+ * When a retained route gets removed from the stack,
148
+ * it'll be kept in routes and can be navigated to again.
149
+ *
150
+ * @param enable Whether to retain the current route in the stack or not.
151
+ */
152
+ retain(enable: boolean): void;
153
+
154
+ /**
155
+ * Dismiss a screen from the stack.
156
+ *
157
+ * @param [count=1] Number of screens to dismiss. Defaults to 1.
158
+ */
159
+ dismiss(count?: number): void;
127
160
  };
128
161
 
129
162
  export const StackActions = {
@@ -148,7 +181,11 @@ export const StackActions = {
148
181
  popToTop() {
149
182
  return { type: 'POP_TO_TOP' } as const satisfies StackActionType;
150
183
  },
151
- popTo(name: string, params?: object, options?: { merge?: boolean }) {
184
+ popTo(
185
+ name: string,
186
+ params?: object,
187
+ options?: { merge?: boolean | undefined }
188
+ ) {
152
189
  return {
153
190
  type: 'POP_TO',
154
191
  payload: {
@@ -158,6 +195,109 @@ export const StackActions = {
158
195
  },
159
196
  } as const satisfies StackActionType;
160
197
  },
198
+ retain(enable: boolean) {
199
+ return {
200
+ type: 'RETAIN',
201
+ payload: { enable },
202
+ } as const satisfies StackActionType;
203
+ },
204
+ dismiss(count: number = 1) {
205
+ return {
206
+ type: 'DISMISS',
207
+ payload: { count },
208
+ } as const satisfies StackActionType;
209
+ },
210
+ };
211
+
212
+ function retainRoutes<ParamList extends ParamListBase>(
213
+ previousState: StackNavigationState<ParamList>,
214
+ nextState: StackNavigationState<ParamList>
215
+ ) {
216
+ if (!nextState.retainedRouteKeys.length) {
217
+ return nextState;
218
+ }
219
+
220
+ const retainedRouteKeySet = new Set(nextState.retainedRouteKeys);
221
+ const nextRouteKeySet = new Set(nextState.routes.map((route) => route.key));
222
+ const retainedRoutes = getActiveRoutes(previousState).filter(
223
+ (route) =>
224
+ retainedRouteKeySet.has(route.key) && !nextRouteKeySet.has(route.key)
225
+ );
226
+
227
+ if (!retainedRoutes.length) {
228
+ return nextState;
229
+ }
230
+
231
+ const removedRetainedRouteKeys = new Set(
232
+ retainedRoutes.map((route) => route.key)
233
+ );
234
+
235
+ return getStateWithRoutes(
236
+ nextState,
237
+ getActiveRoutes(nextState),
238
+ getPreloadedRoutes(nextState),
239
+ retainedRoutes.concat(
240
+ getRetainedRoutes(nextState).filter(
241
+ (route) => !removedRetainedRouteKeys.has(route.key)
242
+ )
243
+ )
244
+ );
245
+ }
246
+
247
+ const getActiveRoutes = <ParamList extends ParamListBase>(
248
+ state: StackNavigationState<ParamList>
249
+ ) => state.routes.slice(0, state.index + 1);
250
+
251
+ const getInactiveRoutes = <ParamList extends ParamListBase>(
252
+ state: StackNavigationState<ParamList>
253
+ ) => state.routes.slice(state.index + 1);
254
+
255
+ const getPreloadedRoutes = <ParamList extends ParamListBase>(
256
+ state: StackNavigationState<ParamList>
257
+ ) => {
258
+ const retainedRouteKeySet = new Set(state.retainedRouteKeys);
259
+
260
+ return getInactiveRoutes(state).filter(
261
+ (route) => !retainedRouteKeySet.has(route.key)
262
+ );
263
+ };
264
+
265
+ const getRetainedRoutes = <ParamList extends ParamListBase>(
266
+ state: StackNavigationState<ParamList>
267
+ ) => {
268
+ const retainedRouteKeySet = new Set(state.retainedRouteKeys);
269
+
270
+ return getInactiveRoutes(state).filter((route) =>
271
+ retainedRouteKeySet.has(route.key)
272
+ );
273
+ };
274
+
275
+ const getStateWithRoutes = <ParamList extends ParamListBase>(
276
+ state: StackNavigationState<ParamList>,
277
+ activeRoutes: Route<string>[],
278
+ preloadedRoutes = getPreloadedRoutes(state),
279
+ retainedRoutes = getRetainedRoutes(state)
280
+ ) => {
281
+ const routeKeySet = new Set(activeRoutes.map((route) => route.key));
282
+ const retainedRouteKeySet = new Set(state.retainedRouteKeys);
283
+
284
+ const filteredRetainedRoutes = retainedRoutes.filter(
285
+ (route) => !routeKeySet.has(route.key)
286
+ );
287
+
288
+ const filteredPreloadedRoutes = preloadedRoutes.filter(
289
+ (route) =>
290
+ !retainedRouteKeySet.has(route.key) && !routeKeySet.has(route.key)
291
+ );
292
+
293
+ return {
294
+ ...state,
295
+ index: activeRoutes.length - 1,
296
+ routes: activeRoutes.concat(
297
+ filteredRetainedRoutes,
298
+ filteredPreloadedRoutes
299
+ ),
300
+ };
161
301
  };
162
302
 
163
303
  export function StackRouter(options: StackRouterOptions) {
@@ -176,13 +316,19 @@ export function StackRouter(options: StackRouterOptions) {
176
316
  ? options.initialRouteName
177
317
  : routeNames[0];
178
318
 
319
+ if (initialRouteName == null) {
320
+ throw new Error(
321
+ "Couldn't initialize the stack navigator. Have you defined any screens?"
322
+ );
323
+ }
324
+
179
325
  return {
180
326
  stale: false,
181
327
  type: 'stack',
182
328
  key: `stack-${nanoid()}`,
183
329
  index: 0,
184
330
  routeNames,
185
- preloadedRoutes: [],
331
+ retainedRouteKeys: [],
186
332
  routes: [
187
333
  {
188
334
  key: `${initialRouteName}-${nanoid()}`,
@@ -214,30 +360,19 @@ export function StackRouter(options: StackRouterOptions) {
214
360
  : route.params,
215
361
  }));
216
362
 
217
- const preloadedRoutes =
218
- state.preloadedRoutes
219
- ?.filter((route) => routeNames.includes(route.name))
220
- .map(
221
- (route) =>
222
- ({
223
- ...route,
224
- key: route.key || `${route.name}-${nanoid()}`,
225
- params:
226
- routeParamList[route.name] !== undefined
227
- ? {
228
- ...routeParamList[route.name],
229
- ...route.params,
230
- }
231
- : route.params,
232
- }) as Route<string>
233
- ) ?? [];
234
-
235
363
  if (routes.length === 0) {
236
364
  const initialRouteName =
237
- options.initialRouteName !== undefined
365
+ options.initialRouteName !== undefined &&
366
+ routeNames.includes(options.initialRouteName)
238
367
  ? options.initialRouteName
239
368
  : routeNames[0];
240
369
 
370
+ if (initialRouteName == null) {
371
+ throw new Error(
372
+ "Couldn't initialize the stack navigator. Have you defined any screens?"
373
+ );
374
+ }
375
+
241
376
  routes.push({
242
377
  key: `${initialRouteName}-${nanoid()}`,
243
378
  name: initialRouteName,
@@ -245,14 +380,23 @@ export function StackRouter(options: StackRouterOptions) {
245
380
  });
246
381
  }
247
382
 
383
+ const index = Math.min(
384
+ Math.max(state.index ?? routes.length - 1, 0),
385
+ routes.length - 1
386
+ );
387
+
388
+ const routeKeys = new Set(routes.map((route) => route.key));
389
+ const retainedRouteKeys =
390
+ state.retainedRouteKeys?.filter((key) => routeKeys.has(key)) ?? [];
391
+
248
392
  return {
249
393
  stale: false,
250
394
  type: 'stack',
251
395
  key: `stack-${nanoid()}`,
252
- index: routes.length - 1,
396
+ index,
253
397
  routeNames,
254
398
  routes,
255
- preloadedRoutes,
399
+ retainedRouteKeys,
256
400
  };
257
401
  },
258
402
 
@@ -260,7 +404,19 @@ export function StackRouter(options: StackRouterOptions) {
260
404
  state,
261
405
  { routeNames, routeParamList, routeKeyChanges }
262
406
  ) {
263
- const routes = state.routes.filter(
407
+ const routes = getActiveRoutes(state).filter(
408
+ (route) =>
409
+ routeNames.includes(route.name) &&
410
+ !routeKeyChanges.includes(route.name)
411
+ );
412
+
413
+ const preloadedRoutes = getPreloadedRoutes(state).filter(
414
+ (route) =>
415
+ routeNames.includes(route.name) &&
416
+ !routeKeyChanges.includes(route.name)
417
+ );
418
+
419
+ const retainedRoutes = getRetainedRoutes(state).filter(
264
420
  (route) =>
265
421
  routeNames.includes(route.name) &&
266
422
  !routeKeyChanges.includes(route.name)
@@ -273,6 +429,12 @@ export function StackRouter(options: StackRouterOptions) {
273
429
  ? options.initialRouteName
274
430
  : routeNames[0];
275
431
 
432
+ if (initialRouteName == null) {
433
+ throw new Error(
434
+ "Couldn't initialize the stack navigator. Have you defined any screens?"
435
+ );
436
+ }
437
+
276
438
  routes.push({
277
439
  key: `${initialRouteName}-${nanoid()}`,
278
440
  name: initialRouteName,
@@ -280,51 +442,73 @@ export function StackRouter(options: StackRouterOptions) {
280
442
  });
281
443
  }
282
444
 
283
- return {
284
- ...state,
285
- routeNames,
445
+ const routeKeys = new Set(
446
+ [...routes, ...preloadedRoutes, ...retainedRoutes].map(
447
+ (route) => route.key
448
+ )
449
+ );
450
+
451
+ const retainedRouteKeys = state.retainedRouteKeys.filter((key) =>
452
+ routeKeys.has(key)
453
+ );
454
+
455
+ return getStateWithRoutes(
456
+ {
457
+ ...state,
458
+ routeNames,
459
+ retainedRouteKeys,
460
+ },
286
461
  routes,
287
- index: Math.min(state.index, routes.length - 1),
288
- };
462
+ preloadedRoutes,
463
+ retainedRoutes
464
+ );
289
465
  },
290
466
 
291
467
  getStateForRouteFocus(state, key) {
292
- const index = state.routes.findIndex((r) => r.key === key);
468
+ const activeRoutes = getActiveRoutes(state);
293
469
 
294
- if (index === -1 || index === state.index) {
470
+ const route = state.routes.find((route) => route.key === key);
471
+
472
+ if (route == null || route === activeRoutes.at(-1)) {
295
473
  return state;
296
474
  }
297
475
 
298
- return {
299
- ...state,
300
- index,
301
- routes: state.routes.slice(0, index + 1),
302
- };
476
+ const index = activeRoutes.indexOf(route);
477
+ const routes =
478
+ index === -1
479
+ ? activeRoutes.concat(route)
480
+ : activeRoutes.slice(0, index + 1);
481
+
482
+ return retainRoutes(state, getStateWithRoutes(state, routes));
303
483
  },
304
484
 
305
485
  getStateForAction(state, action, options) {
306
486
  const { routeParamList } = options;
307
487
 
488
+ const routes = getActiveRoutes(state);
489
+ const preloadedRoutes = getPreloadedRoutes(state);
490
+ const inactiveRoutes = getInactiveRoutes(state);
491
+
308
492
  switch (action.type) {
309
493
  case 'REPLACE': {
494
+ if (!state.routeNames.includes(action.payload.name)) {
495
+ return null;
496
+ }
497
+
310
498
  const currentIndex =
311
- action.target === state.key && action.source
312
- ? state.routes.findIndex((r) => r.key === action.source)
499
+ action.source !== undefined
500
+ ? routes.findIndex((r) => r.key === action.source)
313
501
  : state.index;
314
502
 
315
503
  if (currentIndex === -1) {
316
504
  return null;
317
505
  }
318
506
 
319
- if (!state.routeNames.includes(action.payload.name)) {
320
- return null;
321
- }
322
-
323
507
  const getId = options.routeGetIdList[action.payload.name];
324
508
  const id = getId?.({ params: action.payload.params });
325
509
 
326
- // Re-use preloaded route if available
327
- let route = state.preloadedRoutes.find(
510
+ // Re-use inactive route if available
511
+ let route = inactiveRoutes.find(
328
512
  (route) =>
329
513
  route.name === action.payload.name &&
330
514
  id === getId?.({ params: route.params })
@@ -332,17 +516,22 @@ export function StackRouter(options: StackRouterOptions) {
332
516
 
333
517
  if (!route) {
334
518
  route = createRouteFromAction({ action, routeParamList });
519
+ } else {
520
+ const params = createParamsFromAction({ action, routeParamList });
521
+
522
+ if (route.params !== params) {
523
+ route = { ...route, params };
524
+ }
335
525
  }
336
526
 
337
- return {
338
- ...state,
339
- routes: state.routes.map((r, i) =>
340
- i === currentIndex ? route : r
341
- ),
342
- preloadedRoutes: state.preloadedRoutes.filter(
343
- (r) => r.key !== route.key
344
- ),
345
- };
527
+ return retainRoutes(
528
+ state,
529
+ getStateWithRoutes(
530
+ state,
531
+ routes.map((r, i) => (i === currentIndex ? route : r)),
532
+ preloadedRoutes.filter((r) => r.key !== route.key)
533
+ )
534
+ );
346
535
  }
347
536
 
348
537
  case 'PUSH':
@@ -354,20 +543,41 @@ export function StackRouter(options: StackRouterOptions) {
354
543
  const getId = options.routeGetIdList[action.payload.name];
355
544
  const id = getId?.({ params: action.payload.params });
356
545
 
546
+ let remainingRoutes = routes;
357
547
  let route: Route<string> | undefined;
358
548
 
359
549
  if (action.type === 'NAVIGATE') {
360
- const currentRoute = state.routes[state.index];
550
+ const currentIndex =
551
+ action.source !== undefined
552
+ ? routes.findIndex((r) => r.key === action.source)
553
+ : state.index;
361
554
 
362
- if (id !== undefined) {
363
- if (
364
- currentRoute.name === action.payload.name &&
365
- id === getId?.({ params: currentRoute.params })
366
- ) {
367
- route = currentRoute;
368
- } else if (action.payload.pop) {
369
- for (let i = state.routes.length - 1; i >= 0; i--) {
370
- const r = state.routes[i];
555
+ if (currentIndex === -1) {
556
+ return null;
557
+ }
558
+
559
+ /**
560
+ * When navigating,
561
+ * - If the current route matches the destination, reuse it.
562
+ * - Or, if a route matching the destination exists after the current, reuse it.
563
+ * - Remove all non-matching routes after the current.
564
+ */
565
+ remainingRoutes = routes.slice(0, currentIndex + 1);
566
+ route = routes.find(
567
+ (route, i) =>
568
+ i >= currentIndex &&
569
+ route.name === action.payload.name &&
570
+ (id === undefined || id === getId?.({ params: route.params }))
571
+ );
572
+
573
+ if (!route && action.payload.pop) {
574
+ if (id !== undefined) {
575
+ for (let i = remainingRoutes.length - 1; i >= 0; i--) {
576
+ const r = remainingRoutes[i];
577
+
578
+ if (r == null) {
579
+ throw new Error(`Couldn't find a route at index ${i}.`);
580
+ }
371
581
 
372
582
  if (r.name !== action.payload.name) {
373
583
  continue;
@@ -380,13 +590,21 @@ export function StackRouter(options: StackRouterOptions) {
380
590
 
381
591
  if (r.history?.length) {
382
592
  for (let j = r.history.length - 1; j >= 0; j--) {
593
+ const historyEntry = r.history[j];
594
+
595
+ if (historyEntry == null) {
596
+ throw new Error(
597
+ `Couldn't find a route history entry at index ${j}.`
598
+ );
599
+ }
600
+
383
601
  if (
384
- r.history[j].type === 'params' &&
385
- id === getId?.({ params: r.history[j].params })
602
+ historyEntry.type === 'params' &&
603
+ id === getId?.({ params: historyEntry.params })
386
604
  ) {
387
605
  route = {
388
606
  ...r,
389
- params: r.history[j].params,
607
+ params: historyEntry.params,
390
608
  history: r.history.slice(0, j),
391
609
  };
392
610
  break;
@@ -398,13 +616,8 @@ export function StackRouter(options: StackRouterOptions) {
398
616
  }
399
617
  }
400
618
  }
401
- }
402
- } else {
403
- // If the route matches the current one, then navigate to it
404
- if (action.payload.name === currentRoute.name) {
405
- route = currentRoute;
406
- } else if (action.payload.pop) {
407
- route = state.routes.findLast(
619
+ } else {
620
+ route = remainingRoutes.findLast(
408
621
  (route) => route.name === action.payload.name
409
622
  );
410
623
  }
@@ -412,7 +625,7 @@ export function StackRouter(options: StackRouterOptions) {
412
625
  }
413
626
 
414
627
  if (!route) {
415
- route = state.preloadedRoutes.find(
628
+ route = inactiveRoutes.find(
416
629
  (route) =>
417
630
  route.name === action.payload.name &&
418
631
  id === getId?.({ params: route.params })
@@ -435,43 +648,35 @@ export function StackRouter(options: StackRouterOptions) {
435
648
  params = createParamsFromAction({ action, routeParamList });
436
649
  }
437
650
 
438
- let routes: Route<string>[];
651
+ let nextRoutes: Route<string>[];
439
652
 
440
653
  if (route) {
441
654
  if (action.type === 'NAVIGATE' && action.payload.pop) {
442
- routes = [];
655
+ nextRoutes = [];
443
656
 
444
- // Get all routes until the matching one
445
- for (const r of state.routes) {
657
+ // Get all routes before the matching one
658
+ for (const r of remainingRoutes) {
446
659
  if (r.key === route.key) {
447
- routes.push({
448
- ...route,
449
- path:
450
- action.payload.path !== undefined
451
- ? action.payload.path
452
- : route.path,
453
- params,
454
- });
455
660
  break;
456
661
  }
457
662
 
458
- routes.push(r);
663
+ nextRoutes.push(r);
459
664
  }
460
665
  } else {
461
- routes = state.routes.filter((r) => r.key !== route.key);
462
- routes.push({
463
- ...route,
464
- path:
465
- action.type === 'NAVIGATE' &&
466
- action.payload.path !== undefined
467
- ? action.payload.path
468
- : route.path,
469
- params,
470
- });
666
+ nextRoutes = remainingRoutes.filter((r) => r.key !== route.key);
471
667
  }
668
+
669
+ nextRoutes.push({
670
+ ...route,
671
+ path:
672
+ action.type === 'NAVIGATE' && action.payload.path !== undefined
673
+ ? action.payload.path
674
+ : route.path,
675
+ params,
676
+ });
472
677
  } else {
473
- routes = [
474
- ...state.routes,
678
+ nextRoutes = [
679
+ ...remainingRoutes,
475
680
  {
476
681
  key: `${action.payload.name}-${nanoid()}`,
477
682
  name: action.payload.name,
@@ -482,41 +687,41 @@ export function StackRouter(options: StackRouterOptions) {
482
687
  ];
483
688
  }
484
689
 
485
- return {
486
- ...state,
487
- index: routes.length - 1,
488
- preloadedRoutes: state.preloadedRoutes.filter(
489
- (route) => routes[routes.length - 1].key !== route.key
490
- ),
491
- routes,
492
- };
690
+ return retainRoutes(state, getStateWithRoutes(state, nextRoutes));
493
691
  }
494
692
 
495
693
  case 'POP': {
496
694
  let currentIndex =
497
- action.target === state.key && action.source
498
- ? state.routes.findIndex((r) => r.key === action.source)
695
+ action.source !== undefined
696
+ ? routes.findIndex((r) => r.key === action.source)
499
697
  : state.index;
500
698
 
501
- let route = state.routes[currentIndex];
699
+ if (currentIndex === -1) {
700
+ return null;
701
+ }
702
+
703
+ let route = routes[currentIndex];
704
+
705
+ if (route == null) {
706
+ throw new Error(`Couldn't find a route at index ${currentIndex}.`);
707
+ }
502
708
 
503
709
  /**
504
710
  * When popping entries,
505
711
  * - Pop entries from route.history
506
- * - Pop routes from state.routes
712
+ * - Pop the route itself and continue from the route below it
507
713
  *
508
714
  * Repeat until:
509
715
  * - We have popped the amount of items in count
510
716
  * - There are no more items to pop
717
+ *
718
+ * Routes above the current route are removed.
511
719
  */
512
720
  if (route.history?.length || currentIndex > 0) {
513
721
  let count = action.payload.count;
514
- let routes = state.routes;
515
722
 
516
723
  while (count > 0 && (route.history?.length || currentIndex > 0)) {
517
724
  if (route.history?.length) {
518
- routes = [...routes];
519
-
520
725
  const end = Math.min(count, route.history.length);
521
726
  const last = route.history.at(-end);
522
727
  const history = route.history.slice(0, -end);
@@ -524,7 +729,7 @@ export function StackRouter(options: StackRouterOptions) {
524
729
 
525
730
  count = count - removed;
526
731
 
527
- routes[currentIndex] = {
732
+ route = {
528
733
  ...route,
529
734
  params: last?.type === 'params' ? last.params : route.params,
530
735
  history,
@@ -532,33 +737,70 @@ export function StackRouter(options: StackRouterOptions) {
532
737
  }
533
738
 
534
739
  if (currentIndex > 0 && count > 0) {
535
- const length = routes.length;
536
- const end = Math.max(currentIndex - count + 1, 1);
740
+ count = count - 1;
741
+ currentIndex = currentIndex - 1;
537
742
 
538
- routes = routes
539
- .slice(0, end)
540
- .concat(routes.slice(currentIndex + 1));
743
+ const currentRoute = routes[currentIndex];
541
744
 
542
- const removed = length - routes.length;
745
+ if (currentRoute == null) {
746
+ throw new Error(
747
+ `Couldn't find a route at index ${currentIndex}.`
748
+ );
749
+ }
543
750
 
544
- count = count - removed;
545
- currentIndex = routes.length - 1;
546
- route = routes[currentIndex];
751
+ route = currentRoute;
547
752
  }
548
753
  }
549
754
 
550
- return {
551
- ...state,
552
- index: routes.length - 1,
553
- routes,
554
- };
755
+ const nextRoutes = routes.slice(0, currentIndex + 1);
756
+
757
+ if (route !== routes[currentIndex]) {
758
+ nextRoutes[currentIndex] = route;
759
+ }
760
+
761
+ return retainRoutes(state, getStateWithRoutes(state, nextRoutes));
555
762
  }
556
763
 
557
764
  return null;
558
765
  }
559
766
 
767
+ case 'DISMISS': {
768
+ const currentIndex =
769
+ action.source !== undefined
770
+ ? routes.findIndex((route) => route.key === action.source)
771
+ : state.index;
772
+
773
+ if (
774
+ currentIndex === -1 ||
775
+ routes.length === 1 ||
776
+ action.payload.count < 1
777
+ ) {
778
+ return null;
779
+ }
780
+
781
+ const count = Math.min(
782
+ action.payload.count,
783
+ currentIndex + 1,
784
+ routes.length - 1
785
+ );
786
+
787
+ const firstIndex = currentIndex - count + 1;
788
+
789
+ return retainRoutes(
790
+ state,
791
+ getStateWithRoutes(
792
+ state,
793
+ routes.slice(0, firstIndex).concat(routes.slice(currentIndex + 1))
794
+ )
795
+ );
796
+ }
797
+
560
798
  case 'POP_TO_TOP': {
561
- let route = state.routes[0];
799
+ let route = routes[0];
800
+
801
+ if (route == null) {
802
+ throw new Error("Couldn't find a route at index 0.");
803
+ }
562
804
 
563
805
  if (state.index > 0 || route.history?.length) {
564
806
  if (route.history?.length) {
@@ -568,30 +810,26 @@ export function StackRouter(options: StackRouterOptions) {
568
810
  };
569
811
  }
570
812
 
571
- return {
572
- ...state,
573
- index: 0,
574
- routes: [route],
575
- };
813
+ return retainRoutes(state, getStateWithRoutes(state, [route]));
576
814
  }
577
815
 
578
816
  return null;
579
817
  }
580
818
 
581
819
  case 'POP_TO': {
820
+ if (!state.routeNames.includes(action.payload.name)) {
821
+ return null;
822
+ }
823
+
582
824
  const currentIndex =
583
- action.target === state.key && action.source
584
- ? state.routes.findLastIndex((r) => r.key === action.source)
825
+ action.source !== undefined
826
+ ? routes.findIndex((r) => r.key === action.source)
585
827
  : state.index;
586
828
 
587
829
  if (currentIndex === -1) {
588
830
  return null;
589
831
  }
590
832
 
591
- if (!state.routeNames.includes(action.payload.name)) {
592
- return null;
593
- }
594
-
595
833
  // If the route already exists, navigate to it
596
834
  let index = -1;
597
835
  let historyIndex: number | undefined;
@@ -601,7 +839,11 @@ export function StackRouter(options: StackRouterOptions) {
601
839
 
602
840
  if (id !== undefined) {
603
841
  for (let i = currentIndex; i >= 0; i--) {
604
- const r = state.routes[i];
842
+ const r = routes[i];
843
+
844
+ if (r == null) {
845
+ throw new Error(`Couldn't find a route at index ${i}.`);
846
+ }
605
847
 
606
848
  if (r.name !== action.payload.name) {
607
849
  continue;
@@ -614,9 +856,17 @@ export function StackRouter(options: StackRouterOptions) {
614
856
 
615
857
  if (r.history?.length) {
616
858
  for (let j = r.history.length - 1; j >= 0; j--) {
859
+ const historyEntry = r.history[j];
860
+
861
+ if (historyEntry == null) {
862
+ throw new Error(
863
+ `Couldn't find a route history entry at index ${j}.`
864
+ );
865
+ }
866
+
617
867
  if (
618
- r.history[j].type === 'params' &&
619
- id === getId?.({ params: r.history[j].params })
868
+ historyEntry.type === 'params' &&
869
+ id === getId?.({ params: historyEntry.params })
620
870
  ) {
621
871
  index = i;
622
872
  historyIndex = j;
@@ -629,11 +879,29 @@ export function StackRouter(options: StackRouterOptions) {
629
879
  }
630
880
  }
631
881
  }
632
- } else if (state.routes[currentIndex].name === action.payload.name) {
633
- index = currentIndex;
634
882
  } else {
883
+ const currentRoute = routes[currentIndex];
884
+
885
+ if (currentRoute == null) {
886
+ throw new Error(
887
+ `Couldn't find a route at index ${currentIndex}.`
888
+ );
889
+ }
890
+
891
+ if (currentRoute.name === action.payload.name) {
892
+ index = currentIndex;
893
+ }
894
+ }
895
+
896
+ if (id == null && index === -1) {
635
897
  for (let i = currentIndex; i >= 0; i--) {
636
- if (state.routes[i].name === action.payload.name) {
898
+ const route = routes[i];
899
+
900
+ if (route == null) {
901
+ throw new Error(`Couldn't find a route at index ${i}.`);
902
+ }
903
+
904
+ if (route.name === action.payload.name) {
637
905
  index = i;
638
906
  break;
639
907
  }
@@ -642,8 +910,8 @@ export function StackRouter(options: StackRouterOptions) {
642
910
 
643
911
  // If the route doesn't exist, remove the current route and add the new one
644
912
  if (index === -1) {
645
- // Re-use preloaded route if available
646
- let route = state.preloadedRoutes.find(
913
+ // Re-use inactive route if available
914
+ let route = inactiveRoutes.find(
647
915
  (route) =>
648
916
  route.name === action.payload.name &&
649
917
  id === getId?.({ params: route.params })
@@ -651,27 +919,45 @@ export function StackRouter(options: StackRouterOptions) {
651
919
 
652
920
  if (!route) {
653
921
  route = createRouteFromAction({ action, routeParamList });
922
+ } else {
923
+ const params = createParamsFromAction({ action, routeParamList });
924
+
925
+ if (route.params !== params) {
926
+ route = { ...route, params };
927
+ }
654
928
  }
655
929
 
656
- const routes = state.routes.slice(0, currentIndex).concat(route);
930
+ const nextRoutes = routes.slice(0, currentIndex).concat(route);
657
931
 
658
- return {
659
- ...state,
660
- index: routes.length - 1,
661
- routes,
662
- preloadedRoutes: state.preloadedRoutes.filter(
663
- (r) => r.key !== route.key
664
- ),
665
- };
932
+ return retainRoutes(
933
+ state,
934
+ getStateWithRoutes(
935
+ state,
936
+ nextRoutes,
937
+ preloadedRoutes.filter((r) => r.key !== route.key)
938
+ )
939
+ );
666
940
  }
667
941
 
668
- const route = state.routes[index];
942
+ const route = routes[index];
943
+
944
+ if (route == null) {
945
+ throw new Error(`Couldn't find a route at index ${index}.`);
946
+ }
669
947
 
670
948
  let baseParams = route.params;
671
949
  let history = route.history;
672
950
 
673
951
  if (historyIndex !== undefined && route.history) {
674
- baseParams = route.history[historyIndex].params;
952
+ const historyEntry = route.history[historyIndex];
953
+
954
+ if (historyEntry == null) {
955
+ throw new Error(
956
+ `Couldn't find a route history entry at index ${historyIndex}.`
957
+ );
958
+ }
959
+
960
+ baseParams = historyEntry.params;
675
961
  history = route.history.slice(0, historyIndex);
676
962
  }
677
963
 
@@ -691,73 +977,104 @@ export function StackRouter(options: StackRouterOptions) {
691
977
  params = createParamsFromAction({ action, routeParamList });
692
978
  }
693
979
 
694
- return {
695
- ...state,
696
- index,
697
- routes: [
698
- ...state.routes.slice(0, index),
980
+ return retainRoutes(
981
+ state,
982
+ getStateWithRoutes(state, [
983
+ ...routes.slice(0, index),
699
984
  params !== baseParams || historyIndex !== undefined
700
985
  ? { ...route, params, history }
701
- : state.routes[index],
702
- ],
703
- };
986
+ : route,
987
+ ])
988
+ );
704
989
  }
705
990
 
706
- case 'GO_BACK':
707
- if (state.index > 0 || state.routes[state.index].history?.length) {
708
- return router.getStateForAction(
709
- state,
710
- {
711
- type: 'POP',
712
- payload: { count: 1 },
713
- target: action.target,
714
- source: action.source,
715
- },
716
- options
717
- );
718
- }
991
+ case 'GO_BACK': {
992
+ return router.getStateForAction(
993
+ state,
994
+ {
995
+ type: 'POP',
996
+ payload: { count: 1 },
997
+ target: action.target,
998
+ source: action.source,
999
+ },
1000
+ options
1001
+ );
1002
+ }
719
1003
 
720
- return null;
1004
+ case 'RETAIN': {
1005
+ const route = routes[state.index];
721
1006
 
722
- case 'PRELOAD': {
723
- const getId = options.routeGetIdList[action.payload.name];
724
- const id = getId?.({ params: action.payload.params });
1007
+ if (route == null) {
1008
+ throw new Error(`Couldn't find a route at index ${state.index}.`);
1009
+ }
725
1010
 
726
- let route: Route<string> | undefined;
1011
+ const routeKey = action.source ?? route.key;
727
1012
 
728
- if (id !== undefined) {
729
- route = state.routes.find(
730
- (route) =>
731
- route.name === action.payload.name &&
732
- id === getId?.({ params: route.params })
733
- );
1013
+ if (!state.routes.some((route) => route.key === routeKey)) {
1014
+ return null;
734
1015
  }
735
1016
 
736
- if (route) {
1017
+ if (action.payload.enable) {
1018
+ if (state.retainedRouteKeys.includes(routeKey)) {
1019
+ return state;
1020
+ }
1021
+
737
1022
  return {
738
1023
  ...state,
739
- routes: state.routes.map((r) => {
740
- if (r.key !== route?.key) {
741
- return r;
742
- }
743
- return {
744
- ...r,
745
- params: createParamsFromAction({ action, routeParamList }),
746
- };
747
- }),
1024
+ retainedRouteKeys: [...state.retainedRouteKeys, routeKey],
748
1025
  };
749
- } else {
750
- return {
1026
+ }
1027
+
1028
+ if (!state.retainedRouteKeys.includes(routeKey)) {
1029
+ return state;
1030
+ }
1031
+
1032
+ return getStateWithRoutes(
1033
+ {
751
1034
  ...state,
752
- preloadedRoutes: state.preloadedRoutes
753
- .filter(
754
- (r) =>
755
- r.name !== action.payload.name ||
756
- id !== getId?.({ params: r.params })
757
- )
758
- .concat(createRouteFromAction({ action, routeParamList })),
759
- };
1035
+ retainedRouteKeys: state.retainedRouteKeys.filter(
1036
+ (key) => key !== routeKey
1037
+ ),
1038
+ },
1039
+ routes,
1040
+ preloadedRoutes.filter((route) => route.key !== routeKey)
1041
+ );
1042
+ }
1043
+
1044
+ case 'PRELOAD': {
1045
+ if (!state.routeNames.includes(action.payload.name)) {
1046
+ return null;
1047
+ }
1048
+
1049
+ const getId = options.routeGetIdList[action.payload.name];
1050
+ const id = getId?.({ params: action.payload.params });
1051
+ const params = createParamsFromAction({ action, routeParamList });
1052
+
1053
+ const route = preloadedRoutes.findLast(
1054
+ (route) =>
1055
+ route.name === action.payload.name &&
1056
+ id === getId?.({ params: route.params })
1057
+ );
1058
+
1059
+ if (route) {
1060
+ return getStateWithRoutes(
1061
+ state,
1062
+ routes,
1063
+ preloadedRoutes.map((r) =>
1064
+ r.key === route.key && r.params !== params
1065
+ ? { ...r, params }
1066
+ : r
1067
+ )
1068
+ );
760
1069
  }
1070
+
1071
+ return getStateWithRoutes(
1072
+ state,
1073
+ routes,
1074
+ preloadedRoutes.concat(
1075
+ createRouteFromAction({ action, routeParamList })
1076
+ )
1077
+ );
761
1078
  }
762
1079
 
763
1080
  default:
@@ -765,10 +1082,6 @@ export function StackRouter(options: StackRouterOptions) {
765
1082
  }
766
1083
  },
767
1084
 
768
- getRoutesFromState(state) {
769
- return [...state.routes, ...state.preloadedRoutes];
770
- },
771
-
772
1085
  actionCreators: StackActions,
773
1086
  };
774
1087