@react-navigation/core 7.17.3 → 7.17.5

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.
@@ -29,6 +29,8 @@ type RouteConfig = {
29
29
  params: { screen: string; name?: string; index: number }[];
30
30
  routeNames: string[];
31
31
  parse?: ParseConfig;
32
+ explicitParamNames?: Set<string>;
33
+ hasNestedScreens: boolean;
32
34
  };
33
35
 
34
36
  type InitialRouteConfig = {
@@ -52,6 +54,21 @@ type ConfigResources = {
52
54
  configsByScreen: Record<string, RouteConfig[]>;
53
55
  };
54
56
 
57
+ const NESTED_SCREEN_PARAM_NAMES = [
58
+ 'screen',
59
+ 'params',
60
+ 'initial',
61
+ 'path',
62
+ 'merge',
63
+ 'pop',
64
+ ];
65
+
66
+ const getExplicitParamNames = (parse?: ParseConfig) => {
67
+ const names = Object.entries(parse ?? {}).map(([name]) => name);
68
+
69
+ return names.length ? new Set(names) : undefined;
70
+ };
71
+
55
72
  /**
56
73
  * Utility to parse a path string to initial state object accepted by the container.
57
74
  * This is useful for deep linking when we need to handle the incoming URL.
@@ -105,15 +122,23 @@ export function getStateFromPath<ParamList extends {}>(
105
122
  remaining = remaining.replace(normalizedPrefix, '');
106
123
  }
107
124
 
125
+ const decodedSegments: string[] = [];
126
+
127
+ for (const segment of remaining.split('/')) {
128
+ if (!segment) {
129
+ continue;
130
+ }
131
+
132
+ try {
133
+ decodedSegments.push(decodeURIComponent(segment));
134
+ } catch {
135
+ return undefined;
136
+ }
137
+ }
138
+
108
139
  if (screens === undefined) {
109
140
  // When no config is specified, use the path segments as route names
110
- const routes = remaining
111
- .split('/')
112
- .filter(Boolean)
113
- .map((segment) => {
114
- const name = decodeURIComponent(segment);
115
- return { name };
116
- });
141
+ const routes = decodedSegments.map((name) => ({ name }));
117
142
 
118
143
  if (routes.length) {
119
144
  return createNestedStateObject(path, routes, initialRoutes);
@@ -132,36 +157,26 @@ export function getStateFromPath<ParamList extends {}>(
132
157
  path,
133
158
  match.routeNames.map((name) => ({ name })),
134
159
  initialRoutes,
135
- configs
160
+ match
136
161
  );
137
162
  }
138
163
 
139
164
  return undefined;
140
165
  }
141
166
 
142
- let result: PartialState<NavigationState> | undefined;
143
- let current: PartialState<NavigationState> | undefined;
144
-
145
167
  // We match the whole path against the regex instead of segments
146
168
  // This makes sure matches such as wildcard will catch any unmatched routes, even if nested
147
- const { routes, remainingPath } = matchAgainstConfigs(
169
+ const { routes, config } = matchAgainstConfigs(
148
170
  remaining,
149
171
  configs,
150
172
  configsByScreen
151
173
  );
152
174
 
153
- if (routes !== undefined) {
154
- // This will always be empty if full path matched
155
- current = createNestedStateObject(path, routes, initialRoutes, configs);
156
- remaining = remainingPath;
157
- result = current;
158
- }
159
-
160
- if (current == null || result == null) {
175
+ if (routes === undefined || config === undefined) {
161
176
  return undefined;
162
177
  }
163
178
 
164
- return result;
179
+ return createNestedStateObject(path, routes, initialRoutes, config);
165
180
  }
166
181
 
167
182
  /**
@@ -354,6 +369,7 @@ const matchAgainstConfigs = (
354
369
  ) => {
355
370
  let routes: ParsedRoute[] | undefined;
356
371
  let remainingPath = remaining;
372
+ let matchingConfig: RouteConfig | undefined;
357
373
 
358
374
  // Go through all configs, and see if the next path segment matches our regex
359
375
  for (const config of configs) {
@@ -365,50 +381,57 @@ const matchAgainstConfigs = (
365
381
 
366
382
  // If our regex matches, we need to extract params from the path
367
383
  if (match) {
368
- routes = config.routeNames.map((routeName) => {
384
+ routes = [];
385
+ matchingConfig = config;
386
+
387
+ for (const routeName of config.routeNames) {
369
388
  // Check matching name AND pattern in case same screen is used at different levels in config
370
389
  const routeConfig = configsByScreen[routeName]?.find((c) =>
371
390
  arrayStartsWith(config.segments, c.segments)
372
391
  );
373
392
 
374
- const params =
375
- routeConfig && match.groups
376
- ? Object.fromEntries(
377
- Object.entries(match.groups)
378
- .map(([key, value]) => {
379
- const index = Number(key.replace('param_', ''));
380
- const param = routeConfig.params.find(
381
- (it) => it.index === index
382
- );
383
-
384
- if (param?.screen === routeName && param?.name) {
385
- return [param.name, value];
386
- }
387
-
388
- return null;
389
- })
390
- .filter((it) => it != null)
391
- .map(([key, value]) => {
392
- if (value == null) {
393
- return [key, undefined];
394
- }
395
-
396
- const decoded = decodeURIComponent(value);
397
- const parsed = routeConfig.parse?.[key]
398
- ? routeConfig.parse[key](decoded)
399
- : decoded;
400
-
401
- return [key, parsed];
402
- })
403
- )
404
- : undefined;
393
+ let params: Record<string, unknown> | undefined;
405
394
 
406
- if (params && Object.keys(params).length) {
407
- return { name: routeName, params };
395
+ if (routeConfig && match.groups) {
396
+ const paramEntries: [string, unknown][] = [];
397
+
398
+ for (const [key, value] of Object.entries(match.groups)) {
399
+ const index = Number(key.replace('param_', ''));
400
+ const param = routeConfig.params.find((it) => it.index === index);
401
+
402
+ if (param?.screen !== routeName || !param.name) {
403
+ continue;
404
+ }
405
+
406
+ if (value == null) {
407
+ paramEntries.push([param.name, undefined]);
408
+ continue;
409
+ }
410
+
411
+ let decoded: string;
412
+
413
+ try {
414
+ decoded = decodeURIComponent(value);
415
+ } catch {
416
+ return { routes: undefined, remainingPath };
417
+ }
418
+
419
+ const parser = routeConfig.parse?.[param.name];
420
+
421
+ paramEntries.push([param.name, parser ? parser(decoded) : decoded]);
422
+ }
423
+
424
+ if (paramEntries.length) {
425
+ params = Object.fromEntries(paramEntries);
426
+ }
408
427
  }
409
428
 
410
- return { name: routeName };
411
- });
429
+ if (params && Object.keys(params).length) {
430
+ routes.push({ name: routeName, params });
431
+ } else {
432
+ routes.push({ name: routeName });
433
+ }
434
+ }
412
435
 
413
436
  remainingPath = remainingPath.replace(match[0], '');
414
437
 
@@ -416,7 +439,7 @@ const matchAgainstConfigs = (
416
439
  }
417
440
  }
418
441
 
419
- return { routes, remainingPath };
442
+ return { routes, remainingPath, config: matchingConfig };
420
443
  };
421
444
 
422
445
  const createNormalizedConfigs = (
@@ -442,6 +465,9 @@ const createNormalizedConfigs = (
442
465
  // if an object is specified as the value (e.g. Foo: { ... }),
443
466
  // it can have `path` property and
444
467
  // it could have `screens` prop which has nested configs
468
+ const nestedScreens = config.screens;
469
+ const hasNestedScreens = !!nestedScreens;
470
+
445
471
  if (typeof config.path === 'string') {
446
472
  if (config.exact && config.path == null) {
447
473
  throw new Error(
@@ -461,7 +487,8 @@ const createNormalizedConfigs = (
461
487
  screen,
462
488
  [...routeNames],
463
489
  [...paths, { screen, path: alias }],
464
- config.parse
490
+ config.parse,
491
+ hasNestedScreens
465
492
  )
466
493
  );
467
494
  } else if (typeof alias === 'object') {
@@ -472,7 +499,8 @@ const createNormalizedConfigs = (
472
499
  alias.exact
473
500
  ? [{ screen, path: alias.path }]
474
501
  : [...paths, { screen, path: alias.path }],
475
- alias.parse
502
+ alias.parse,
503
+ hasNestedScreens
476
504
  )
477
505
  );
478
506
  }
@@ -487,7 +515,13 @@ const createNormalizedConfigs = (
487
515
 
488
516
  paths.push({ screen, path: config.path });
489
517
  configs.push(
490
- createConfigItem(screen, [...routeNames], [...paths], config.parse)
518
+ createConfigItem(
519
+ screen,
520
+ [...routeNames],
521
+ [...paths],
522
+ config.parse,
523
+ hasNestedScreens
524
+ )
491
525
  );
492
526
 
493
527
  configs.push(...aliasConfigs);
@@ -503,7 +537,7 @@ const createNormalizedConfigs = (
503
537
  );
504
538
  }
505
539
 
506
- if (config.screens) {
540
+ if (nestedScreens) {
507
541
  // property `initialRouteName` without `screens` has no purpose
508
542
  if (config.initialRouteName) {
509
543
  initials.push({
@@ -512,10 +546,10 @@ const createNormalizedConfigs = (
512
546
  });
513
547
  }
514
548
 
515
- Object.keys(config.screens).forEach((nestedConfig) => {
549
+ Object.keys(nestedScreens).forEach((nestedConfig) => {
516
550
  const result = createNormalizedConfigs(
517
551
  nestedConfig,
518
- config.screens as Record<string, string | PathConfig<ParamListBase>>,
552
+ nestedScreens as Record<string, string | PathConfig<ParamListBase>>,
519
553
  initials,
520
554
  [...paths],
521
555
  [...parentScreens],
@@ -536,7 +570,8 @@ const createConfigItem = (
536
570
  screen: string,
537
571
  routeNames: string[],
538
572
  paths: { screen: string; path: string }[],
539
- parse?: ParseConfig
573
+ parse?: ParseConfig,
574
+ hasNestedScreens = false
540
575
  ): RouteConfig => {
541
576
  const parts: (PatternPart & { screen: string })[] = [];
542
577
 
@@ -581,22 +616,11 @@ const createConfigItem = (
581
616
  params,
582
617
  routeNames,
583
618
  parse,
619
+ explicitParamNames: getExplicitParamNames(parse),
620
+ hasNestedScreens,
584
621
  };
585
622
  };
586
623
 
587
- const findParseConfigForRoute = (
588
- routeName: string,
589
- flatConfig: RouteConfig[]
590
- ): ParseConfig | undefined => {
591
- for (const config of flatConfig) {
592
- if (routeName === config.routeNames[config.routeNames.length - 1]) {
593
- return config.parse;
594
- }
595
- }
596
-
597
- return undefined;
598
- };
599
-
600
624
  // Try to find an initial route connected with the one passed
601
625
  const findInitialRoute = (
602
626
  routeName: string,
@@ -658,7 +682,7 @@ const createNestedStateObject = (
658
682
  path: string,
659
683
  routes: ParsedRoute[],
660
684
  initialRoutes: InitialRouteConfig[],
661
- flatConfig?: RouteConfig[]
685
+ routeConfig?: RouteConfig
662
686
  ) => {
663
687
  let route = routes.shift() as ParsedRoute;
664
688
  const parentScreens: string[] = [];
@@ -702,7 +726,10 @@ const createNestedStateObject = (
702
726
 
703
727
  const params = parseQueryParams(
704
728
  path,
705
- flatConfig ? findParseConfigForRoute(route.name, flatConfig) : undefined
729
+ routeConfig?.parse,
730
+ routeConfig?.explicitParamNames,
731
+ routeConfig?.hasNestedScreens,
732
+ route.params
706
733
  );
707
734
 
708
735
  if (params) {
@@ -714,7 +741,10 @@ const createNestedStateObject = (
714
741
 
715
742
  const parseQueryParams = (
716
743
  path: string,
717
- parseConfig?: Record<string, (value: string) => unknown>
744
+ parseConfig?: ParseConfig,
745
+ explicitParamNames?: Set<string>,
746
+ hasNestedScreens = false,
747
+ routeParams?: Record<string, unknown>
718
748
  ) => {
719
749
  const query = path.split('?')[1];
720
750
  const params: Record<string, unknown> = queryString.parse(query);
@@ -730,5 +760,19 @@ const parseQueryParams = (
730
760
  });
731
761
  }
732
762
 
763
+ if (
764
+ hasNestedScreens &&
765
+ !explicitParamNames?.has('screen') &&
766
+ (typeof params.screen === 'string' ||
767
+ typeof routeParams?.screen === 'string')
768
+ ) {
769
+ for (const name of NESTED_SCREEN_PARAM_NAMES) {
770
+ if (!explicitParamNames?.has(name)) {
771
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
772
+ delete params[name];
773
+ }
774
+ }
775
+ }
776
+
733
777
  return Object.keys(params).length ? params : undefined;
734
778
  };
@@ -63,6 +63,14 @@ type NavigatorRoute = {
63
63
  params?: NavigatorScreenParams<ParamListBase>;
64
64
  };
65
65
 
66
+ const isNavigationState = (
67
+ state: unknown
68
+ ): state is NavigationState | PartialState<NavigationState> =>
69
+ state != null &&
70
+ typeof state === 'object' &&
71
+ 'routes' in state &&
72
+ Array.isArray(state.routes);
73
+
66
74
  const isScreen = (
67
75
  child: React.ReactElement<unknown>
68
76
  ): child is React.ReactElement<{
@@ -279,8 +287,10 @@ const getRouteConfigsFromChildren = <
279
287
  };
280
288
 
281
289
  const getStateFromParams = (params: NavigatorRoute['params']) => {
282
- if (params?.state != null) {
283
- return params.state;
290
+ const state = params?.state;
291
+
292
+ if (isNavigationState(state)) {
293
+ return state;
284
294
  } else if (typeof params?.screen === 'string' && params?.initial !== false) {
285
295
  return {
286
296
  routes: [
@@ -647,24 +657,21 @@ export function useNavigationBuilder<
647
657
 
648
658
  if (route?.params && !didConsumeNestedParams) {
649
659
  let action: CommonActions.Action | undefined;
660
+ const stateFromParams = route.params.state;
650
661
 
651
- if (
652
- typeof route.params.state === 'object' &&
653
- route.params.state != null &&
654
- !isNestedParamsConsumed
655
- ) {
662
+ if (isNavigationState(stateFromParams) && !isNestedParamsConsumed) {
656
663
  didConsumeNestedParams = true;
657
664
 
658
665
  if (
659
666
  options.UNSTABLE_routeNamesChangeBehavior === 'lastUnhandled' &&
660
- doesStateHaveOnlyInvalidRoutes(route.params.state)
667
+ doesStateHaveOnlyInvalidRoutes(stateFromParams)
661
668
  ) {
662
- if (route.params.state !== unhandledState) {
663
- setUnhandledState(route.params.state);
669
+ if (stateFromParams !== unhandledState) {
670
+ setUnhandledState(stateFromParams);
664
671
  }
665
672
  } else {
666
673
  // If the route was updated with new state, we should reset to it
667
- action = CommonActions.reset(route.params.state);
674
+ action = CommonActions.reset(stateFromParams);
668
675
  }
669
676
  } else if (
670
677
  typeof route.params.screen === 'string' &&