@react-navigation/routers 8.0.0-alpha.1 → 8.0.0-alpha.11

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