@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.
- package/lib/module/StaticNavigation.js +61 -20
- package/lib/module/StaticNavigation.js.map +1 -1
- package/lib/module/getStateFromPath.js +87 -58
- package/lib/module/getStateFromPath.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +10 -7
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/typescript/src/StaticNavigation.d.ts +16 -10
- package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/StaticNavigation.tsx +145 -53
- package/src/getStateFromPath.tsx +125 -81
- package/src/useNavigationBuilder.tsx +18 -11
package/src/getStateFromPath.tsx
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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,
|
|
169
|
+
const { routes, config } = matchAgainstConfigs(
|
|
148
170
|
remaining,
|
|
149
171
|
configs,
|
|
150
172
|
configsByScreen
|
|
151
173
|
);
|
|
152
174
|
|
|
153
|
-
if (routes
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
407
|
-
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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(
|
|
549
|
+
Object.keys(nestedScreens).forEach((nestedConfig) => {
|
|
516
550
|
const result = createNormalizedConfigs(
|
|
517
551
|
nestedConfig,
|
|
518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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
|
-
|
|
283
|
-
|
|
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(
|
|
667
|
+
doesStateHaveOnlyInvalidRoutes(stateFromParams)
|
|
661
668
|
) {
|
|
662
|
-
if (
|
|
663
|
-
setUnhandledState(
|
|
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(
|
|
674
|
+
action = CommonActions.reset(stateFromParams);
|
|
668
675
|
}
|
|
669
676
|
} else if (
|
|
670
677
|
typeof route.params.screen === 'string' &&
|