@react-navigation/core 7.21.2 → 7.21.4
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/PreventRemoveContext.js.map +1 -1
- package/lib/module/PreventRemoveProvider.js +34 -24
- package/lib/module/PreventRemoveProvider.js.map +1 -1
- package/lib/module/createNavigationContainerRef.js +6 -8
- package/lib/module/createNavigationContainerRef.js.map +1 -1
- package/lib/module/getStateFromPath.js +82 -58
- package/lib/module/getStateFromPath.js.map +1 -1
- package/lib/module/useEventEmitter.js +1 -1
- package/lib/module/useEventEmitter.js.map +1 -1
- package/lib/module/useKeyedChildListeners.js +4 -2
- package/lib/module/useKeyedChildListeners.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +3 -15
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useNavigationCache.js +5 -2
- package/lib/module/useNavigationCache.js.map +1 -1
- package/lib/module/useNavigationHelpers.js +27 -9
- package/lib/module/useNavigationHelpers.js.map +1 -1
- package/lib/module/useNavigationState.js +1 -1
- package/lib/module/useNavigationState.js.map +1 -1
- package/lib/module/useOnAction.js +1 -1
- package/lib/module/useOnAction.js.map +1 -1
- package/lib/module/useOptionsGetters.js +3 -1
- package/lib/module/useOptionsGetters.js.map +1 -1
- package/lib/module/usePreventRemove.js +9 -2
- package/lib/module/usePreventRemove.js.map +1 -1
- package/lib/module/useRouteCache.js +14 -7
- package/lib/module/useRouteCache.js.map +1 -1
- package/lib/module/useScheduleUpdate.js +8 -8
- package/lib/module/useScheduleUpdate.js.map +1 -1
- package/lib/module/useSyncState.js +4 -1
- package/lib/module/useSyncState.js.map +1 -1
- package/lib/typescript/src/PreventRemoveContext.d.ts +1 -0
- package/lib/typescript/src/PreventRemoveContext.d.ts.map +1 -1
- package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
- package/lib/typescript/src/StaticNavigation.d.ts +2 -2
- package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
- package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +3 -3
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationHelpers.d.ts +2 -3
- package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
- package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
- package/lib/typescript/src/usePreventRemove.d.ts.map +1 -1
- package/lib/typescript/src/usePreventRemoveContext.d.ts +1 -0
- package/lib/typescript/src/usePreventRemoveContext.d.ts.map +1 -1
- package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
- package/lib/typescript/src/useScheduleUpdate.d.ts +5 -4
- package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
- package/lib/typescript/src/useSyncState.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/PreventRemoveContext.tsx +1 -0
- package/src/PreventRemoveProvider.tsx +48 -43
- package/src/StaticNavigation.tsx +2 -2
- package/src/createNavigationContainerRef.tsx +12 -9
- package/src/getStateFromPath.tsx +112 -74
- package/src/types.tsx +18 -16
- package/src/useEventEmitter.tsx +1 -1
- package/src/useKeyedChildListeners.tsx +4 -2
- package/src/useNavigationBuilder.tsx +2 -16
- package/src/useNavigationCache.tsx +6 -2
- package/src/useNavigationHelpers.tsx +26 -9
- package/src/useNavigationState.tsx +1 -1
- package/src/useOnAction.tsx +1 -1
- package/src/useOptionsGetters.tsx +4 -1
- package/src/usePreventRemove.tsx +11 -2
- package/src/useRouteCache.tsx +19 -11
- package/src/useScheduleUpdate.tsx +8 -7
- package/src/useSyncState.tsx +5 -1
package/src/getStateFromPath.tsx
CHANGED
|
@@ -26,10 +26,11 @@ type RouteConfig = {
|
|
|
26
26
|
screen: string;
|
|
27
27
|
regex?: RegExp;
|
|
28
28
|
segments: string[];
|
|
29
|
-
params: { screen: string; name
|
|
29
|
+
params: { screen: string; name: string; index: number }[];
|
|
30
30
|
routeNames: string[];
|
|
31
31
|
parse?: ParseConfig;
|
|
32
32
|
explicitParamNames?: Set<string>;
|
|
33
|
+
pathParamNames: Set<string>;
|
|
33
34
|
hasNestedScreens: boolean;
|
|
34
35
|
};
|
|
35
36
|
|
|
@@ -122,23 +123,21 @@ export function getStateFromPath<ParamList extends {}>(
|
|
|
122
123
|
remaining = remaining.replace(normalizedPrefix, '');
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
|
|
126
|
+
if (screens === undefined) {
|
|
127
|
+
// When no config is specified, use the path segments as route names
|
|
128
|
+
const routes: ParsedRoute[] = [];
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
for (const segment of remaining.split('/')) {
|
|
131
|
+
if (!segment) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
try {
|
|
136
|
+
routes.push({ name: decodeURIComponent(segment) });
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
136
140
|
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (screens === undefined) {
|
|
140
|
-
// When no config is specified, use the path segments as route names
|
|
141
|
-
const routes = decodedSegments.map((name) => ({ name }));
|
|
142
141
|
|
|
143
142
|
if (routes.length) {
|
|
144
143
|
return createNestedStateObject(path, routes, initialRoutes);
|
|
@@ -166,9 +165,21 @@ export function getStateFromPath<ParamList extends {}>(
|
|
|
166
165
|
|
|
167
166
|
// We match the whole path against the regex instead of segments
|
|
168
167
|
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
|
|
168
|
+
const firstRawSegment = remaining.split('/')[0];
|
|
169
|
+
|
|
170
|
+
let firstDecodedSegment: string | undefined;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
firstDecodedSegment =
|
|
174
|
+
firstRawSegment != null ? decodeURIComponent(firstRawSegment) : undefined;
|
|
175
|
+
} catch {
|
|
176
|
+
firstDecodedSegment = undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
169
179
|
const { routes, config } = matchAgainstConfigs(
|
|
170
180
|
remaining,
|
|
171
|
-
|
|
181
|
+
firstDecodedSegment,
|
|
182
|
+
firstRawSegment,
|
|
172
183
|
configs,
|
|
173
184
|
configsByScreen
|
|
174
185
|
);
|
|
@@ -209,12 +220,17 @@ function prepareConfigResources(options?: Options<{}>) {
|
|
|
209
220
|
const initialRoutes = getInitialRoutes(options);
|
|
210
221
|
const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
|
|
211
222
|
|
|
212
|
-
checkForDuplicatedConfigs(configs);
|
|
213
|
-
|
|
214
223
|
const configsByScreen: Record<string, RouteConfig[]> = {};
|
|
224
|
+
const configsByPattern = new Map<string, RouteConfig>();
|
|
215
225
|
|
|
216
226
|
for (const c of configs) {
|
|
217
227
|
(configsByScreen[c.screen] ??= []).push(c);
|
|
228
|
+
|
|
229
|
+
const pattern = c.segments.join('/');
|
|
230
|
+
const existing = configsByPattern.get(pattern);
|
|
231
|
+
|
|
232
|
+
checkForDuplicatedConfigs(existing, c, pattern);
|
|
233
|
+
configsByPattern.set(pattern, c);
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
return {
|
|
@@ -330,50 +346,45 @@ function getSortedNormalizedConfigs(
|
|
|
330
346
|
});
|
|
331
347
|
}
|
|
332
348
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// It's not a problem if the path string omitted from a inner most screen
|
|
343
|
-
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
|
|
344
|
-
const intersects =
|
|
345
|
-
a.length > b.length
|
|
346
|
-
? b.every((it, i) => a[i] === it)
|
|
347
|
-
: a.every((it, i) => b[i] === it);
|
|
348
|
-
|
|
349
|
-
if (!intersects) {
|
|
350
|
-
throw new Error(
|
|
351
|
-
`Found conflicting screens with the same pattern. The pattern '${
|
|
352
|
-
pattern
|
|
353
|
-
}' resolves to both '${a.join(' > ')}' and '${b.join(
|
|
354
|
-
' > '
|
|
355
|
-
)}'. Patterns must be unique and cannot resolve to more than one screen.`
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
349
|
+
// Throw if two configs resolve to the same pattern but conflicting screens
|
|
350
|
+
function checkForDuplicatedConfigs(
|
|
351
|
+
existing: RouteConfig | undefined,
|
|
352
|
+
config: RouteConfig,
|
|
353
|
+
pattern: string
|
|
354
|
+
) {
|
|
355
|
+
if (!existing) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
359
358
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
359
|
+
const a = existing.routeNames;
|
|
360
|
+
const b = config.routeNames;
|
|
361
|
+
|
|
362
|
+
// It's not a problem if the path string omitted from a inner most screen
|
|
363
|
+
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
|
|
364
|
+
const intersects =
|
|
365
|
+
a.length > b.length ? arrayStartsWith(a, b) : arrayStartsWith(b, a);
|
|
366
|
+
|
|
367
|
+
if (!intersects) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Found conflicting screens with the same pattern. The pattern '${
|
|
370
|
+
pattern
|
|
371
|
+
}' resolves to both '${a.join(' > ')}' and '${b.join(
|
|
372
|
+
' > '
|
|
373
|
+
)}'. Patterns must be unique and cannot resolve to more than one screen.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
364
376
|
}
|
|
365
377
|
|
|
366
378
|
const matchAgainstConfigs = (
|
|
367
379
|
remaining: string,
|
|
368
|
-
|
|
380
|
+
firstDecodedSegment: string | undefined,
|
|
381
|
+
firstRawSegment: string | undefined,
|
|
369
382
|
configs: RouteConfig[],
|
|
370
383
|
configsByScreen: Record<string, RouteConfig[]>
|
|
371
384
|
) => {
|
|
372
385
|
let routes: ParsedRoute[] | undefined;
|
|
373
386
|
let remainingPath = remaining;
|
|
374
387
|
let matchingConfig: RouteConfig | undefined;
|
|
375
|
-
const firstDecodedSegment = decodedSegments[0];
|
|
376
|
-
const firstRawSegment = remainingPath.split('/')[0];
|
|
377
388
|
|
|
378
389
|
// Go through all configs, and see if the next path segment matches our regex
|
|
379
390
|
for (const config of configs) {
|
|
@@ -395,8 +406,8 @@ const matchAgainstConfigs = (
|
|
|
395
406
|
|
|
396
407
|
// If our regex matches, we need to extract params from the path
|
|
397
408
|
if (match) {
|
|
398
|
-
|
|
399
|
-
|
|
409
|
+
const matchedRoutes: ParsedRoute[] = [];
|
|
410
|
+
let hasInvalidParam = false;
|
|
400
411
|
|
|
401
412
|
for (const routeName of config.routeNames) {
|
|
402
413
|
// Check matching name AND pattern in case same screen is used at different levels in config
|
|
@@ -409,14 +420,13 @@ const matchAgainstConfigs = (
|
|
|
409
420
|
if (routeConfig && match.groups) {
|
|
410
421
|
const paramEntries: [string, unknown][] = [];
|
|
411
422
|
|
|
412
|
-
for (const
|
|
413
|
-
|
|
414
|
-
const param = routeConfig.params.find((it) => it.index === index);
|
|
415
|
-
|
|
416
|
-
if (param?.screen !== routeName || !param.name) {
|
|
423
|
+
for (const param of routeConfig.params) {
|
|
424
|
+
if (param.screen !== routeName) {
|
|
417
425
|
continue;
|
|
418
426
|
}
|
|
419
427
|
|
|
428
|
+
const value = match.groups[`param_${param.index}`];
|
|
429
|
+
|
|
420
430
|
if (value == null) {
|
|
421
431
|
paramEntries.push([param.name, undefined]);
|
|
422
432
|
continue;
|
|
@@ -427,7 +437,8 @@ const matchAgainstConfigs = (
|
|
|
427
437
|
try {
|
|
428
438
|
decoded = decodeURIComponent(value);
|
|
429
439
|
} catch {
|
|
430
|
-
|
|
440
|
+
hasInvalidParam = true;
|
|
441
|
+
break;
|
|
431
442
|
}
|
|
432
443
|
|
|
433
444
|
const parser = routeConfig.parse?.[param.name];
|
|
@@ -435,18 +446,28 @@ const matchAgainstConfigs = (
|
|
|
435
446
|
paramEntries.push([param.name, parser ? parser(decoded) : decoded]);
|
|
436
447
|
}
|
|
437
448
|
|
|
449
|
+
if (hasInvalidParam) {
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
|
|
438
453
|
if (paramEntries.length) {
|
|
439
454
|
params = Object.fromEntries(paramEntries);
|
|
440
455
|
}
|
|
441
456
|
}
|
|
442
457
|
|
|
443
458
|
if (params && Object.keys(params).length) {
|
|
444
|
-
|
|
459
|
+
matchedRoutes.push({ name: routeName, params });
|
|
445
460
|
} else {
|
|
446
|
-
|
|
461
|
+
matchedRoutes.push({ name: routeName });
|
|
447
462
|
}
|
|
448
463
|
}
|
|
449
464
|
|
|
465
|
+
if (hasInvalidParam) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
routes = matchedRoutes;
|
|
470
|
+
matchingConfig = config;
|
|
450
471
|
remainingPath = remainingPath.replace(match[0], '');
|
|
451
472
|
|
|
452
473
|
break;
|
|
@@ -610,8 +631,10 @@ const createConfigItem = (
|
|
|
610
631
|
const parts: (PatternPart & { screen: string })[] = [];
|
|
611
632
|
|
|
612
633
|
// Parse the path string into parts for easier matching
|
|
613
|
-
for (const { screen, path } of paths) {
|
|
614
|
-
parts.push(
|
|
634
|
+
for (const { screen: pathScreen, path } of paths) {
|
|
635
|
+
parts.push(
|
|
636
|
+
...getPatternParts(path).map((part) => ({ ...part, screen: pathScreen }))
|
|
637
|
+
);
|
|
615
638
|
}
|
|
616
639
|
|
|
617
640
|
const regex = parts.length
|
|
@@ -631,17 +654,24 @@ const createConfigItem = (
|
|
|
631
654
|
: undefined;
|
|
632
655
|
|
|
633
656
|
const segments = parts.map((it) => it.segment);
|
|
634
|
-
const params =
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
657
|
+
const params: RouteConfig['params'] = [];
|
|
658
|
+
const pathParamNames = new Set<string>();
|
|
659
|
+
|
|
660
|
+
for (const [index, part] of parts.entries()) {
|
|
661
|
+
if (!part.param) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
params.push({
|
|
666
|
+
index,
|
|
667
|
+
screen: part.screen,
|
|
668
|
+
name: part.param,
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
if (part.screen === screen) {
|
|
672
|
+
pathParamNames.add(part.param);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
645
675
|
|
|
646
676
|
return {
|
|
647
677
|
screen,
|
|
@@ -651,6 +681,7 @@ const createConfigItem = (
|
|
|
651
681
|
routeNames,
|
|
652
682
|
parse,
|
|
653
683
|
explicitParamNames: getExplicitParamNames(parse),
|
|
684
|
+
pathParamNames,
|
|
654
685
|
hasNestedScreens,
|
|
655
686
|
};
|
|
656
687
|
};
|
|
@@ -761,6 +792,7 @@ const createNestedStateObject = (
|
|
|
761
792
|
const params = parseQueryParams(
|
|
762
793
|
path,
|
|
763
794
|
routeConfig?.parse,
|
|
795
|
+
routeConfig?.pathParamNames,
|
|
764
796
|
routeConfig?.explicitParamNames,
|
|
765
797
|
routeConfig?.hasNestedScreens,
|
|
766
798
|
route.params
|
|
@@ -776,6 +808,7 @@ const createNestedStateObject = (
|
|
|
776
808
|
const parseQueryParams = (
|
|
777
809
|
path: string,
|
|
778
810
|
parseConfig?: ParseConfig,
|
|
811
|
+
pathParamNames: Set<string> = new Set(),
|
|
779
812
|
explicitParamNames?: Set<string>,
|
|
780
813
|
hasNestedScreens = false,
|
|
781
814
|
routeParams?: Record<string, unknown>
|
|
@@ -783,6 +816,11 @@ const parseQueryParams = (
|
|
|
783
816
|
const query = path.split('?')[1];
|
|
784
817
|
const params: Record<string, unknown> = queryString.parse(query);
|
|
785
818
|
|
|
819
|
+
for (const name of pathParamNames) {
|
|
820
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
821
|
+
delete params[name];
|
|
822
|
+
}
|
|
823
|
+
|
|
786
824
|
if (parseConfig) {
|
|
787
825
|
Object.keys(params).forEach((name) => {
|
|
788
826
|
if (
|
package/src/types.tsx
CHANGED
|
@@ -1103,21 +1103,23 @@ export type NavigatorTypeBag<
|
|
|
1103
1103
|
Navigator: Navigator;
|
|
1104
1104
|
};
|
|
1105
1105
|
|
|
1106
|
-
type
|
|
1106
|
+
type TypedNavigatorStaticComponent<Bag extends NavigatorTypeBagBase> =
|
|
1107
1107
|
React.ComponentType<
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1108
|
+
Partial<
|
|
1109
|
+
Omit<
|
|
1110
|
+
React.ComponentProps<
|
|
1111
|
+
TypedNavigatorInternal<
|
|
1112
|
+
Bag['ParamList'],
|
|
1113
|
+
Bag['NavigatorID'],
|
|
1114
|
+
Bag['State'],
|
|
1115
|
+
Bag['ScreenOptions'],
|
|
1116
|
+
Bag['EventMap'],
|
|
1117
|
+
Bag['NavigationList'],
|
|
1118
|
+
Bag['Navigator']
|
|
1119
|
+
>['Navigator']
|
|
1120
|
+
>,
|
|
1121
|
+
'children'
|
|
1122
|
+
>
|
|
1121
1123
|
>
|
|
1122
1124
|
>;
|
|
1123
1125
|
|
|
@@ -1152,10 +1154,10 @@ type TypedNavigatorStatic<
|
|
|
1152
1154
|
config: Config;
|
|
1153
1155
|
with: (
|
|
1154
1156
|
Component: React.ComponentType<{
|
|
1155
|
-
Navigator:
|
|
1157
|
+
Navigator: TypedNavigatorStaticComponent<Bag>;
|
|
1156
1158
|
}>
|
|
1157
1159
|
) => TypedNavigatorStaticDecorated<Bag, Config>;
|
|
1158
|
-
getComponent: () =>
|
|
1160
|
+
getComponent: () => TypedNavigatorStaticComponent<Bag>;
|
|
1159
1161
|
} & PrivateValueStore<[Bag['ParamList'], Bag['NavigationList'], unknown]>;
|
|
1160
1162
|
|
|
1161
1163
|
export type TypedNavigator<
|
package/src/useEventEmitter.tsx
CHANGED
|
@@ -19,7 +19,7 @@ export function useEventEmitter<T extends Record<string, any>>(
|
|
|
19
19
|
const listenRef = React.useRef(listen);
|
|
20
20
|
const onPostEmitRef = React.useRef(onPostEmit);
|
|
21
21
|
|
|
22
|
-
React.
|
|
22
|
+
React.useInsertionEffect(() => {
|
|
23
23
|
listenRef.current = listen;
|
|
24
24
|
onPostEmitRef.current = onPostEmit;
|
|
25
25
|
});
|
|
@@ -28,8 +28,10 @@ export function useKeyedChildListeners() {
|
|
|
28
28
|
keyedListeners[type][key] = listener;
|
|
29
29
|
|
|
30
30
|
return () => {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if (keyedListeners[type][key] === listener) {
|
|
32
|
+
// @ts-expect-error: according to ref stated above you can use `key` to index type
|
|
33
|
+
keyedListeners[type][key] = undefined;
|
|
34
|
+
}
|
|
33
35
|
};
|
|
34
36
|
},
|
|
35
37
|
[keyedListeners]
|
|
@@ -37,7 +37,6 @@ import {
|
|
|
37
37
|
} from './types';
|
|
38
38
|
import { UnhandledActionContext } from './UnhandledActionContext';
|
|
39
39
|
import { useChildListeners } from './useChildListeners';
|
|
40
|
-
import { useClientLayoutEffect } from './useClientLayoutEffect';
|
|
41
40
|
import { useComponent } from './useComponent';
|
|
42
41
|
import { useCurrentRender } from './useCurrentRender';
|
|
43
42
|
import { type ScreenConfigWithParent, useDescriptors } from './useDescriptors';
|
|
@@ -588,7 +587,7 @@ export function useNavigationBuilder<
|
|
|
588
587
|
|
|
589
588
|
const previousRouteKeyListRef = React.useRef(routeKeyList);
|
|
590
589
|
|
|
591
|
-
React.
|
|
590
|
+
React.useInsertionEffect(() => {
|
|
592
591
|
previousRouteKeyListRef.current = routeKeyList;
|
|
593
592
|
});
|
|
594
593
|
|
|
@@ -794,19 +793,6 @@ export function useNavigationBuilder<
|
|
|
794
793
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
795
794
|
}, []);
|
|
796
795
|
|
|
797
|
-
// In some cases (e.g. route names change), internal state might have changed
|
|
798
|
-
// But it hasn't been committed yet, so hasn't propagated to the sync external store
|
|
799
|
-
// During this time, we need to return the internal state in `getState`
|
|
800
|
-
// Otherwise it can result in inconsistent state during render in children
|
|
801
|
-
// To avoid this, we use a ref for render phase, and immediately clear it on commit
|
|
802
|
-
const stateRef = React.useRef<State | null>(state);
|
|
803
|
-
|
|
804
|
-
stateRef.current = state;
|
|
805
|
-
|
|
806
|
-
useClientLayoutEffect(() => {
|
|
807
|
-
stateRef.current = null;
|
|
808
|
-
});
|
|
809
|
-
|
|
810
796
|
const getState = useLatestCallback((): State => {
|
|
811
797
|
const currentState = getCurrentState();
|
|
812
798
|
|
|
@@ -955,9 +941,9 @@ export function useNavigationBuilder<
|
|
|
955
941
|
onAction,
|
|
956
942
|
onUnhandledAction,
|
|
957
943
|
getState,
|
|
944
|
+
state,
|
|
958
945
|
emitter,
|
|
959
946
|
router,
|
|
960
|
-
stateRef,
|
|
961
947
|
});
|
|
962
948
|
|
|
963
949
|
useFocusedListenersChildrenAdapter({
|
|
@@ -134,7 +134,7 @@ export function useNavigationCache<
|
|
|
134
134
|
[base, getState, navigation, setOptions, emitter]
|
|
135
135
|
);
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
const navigations = state.routes.reduce<
|
|
138
138
|
NavigationCache<State, ScreenOptions, EventMap>
|
|
139
139
|
>((acc, route) => {
|
|
140
140
|
const previous = cache.current[route.key];
|
|
@@ -233,8 +233,12 @@ export function useNavigationCache<
|
|
|
233
233
|
return acc;
|
|
234
234
|
}, {});
|
|
235
235
|
|
|
236
|
+
React.useInsertionEffect(() => {
|
|
237
|
+
cache.current = navigations;
|
|
238
|
+
});
|
|
239
|
+
|
|
236
240
|
return {
|
|
237
241
|
base,
|
|
238
|
-
navigations
|
|
242
|
+
navigations,
|
|
239
243
|
};
|
|
240
244
|
}
|
|
@@ -20,9 +20,9 @@ type Options<State extends NavigationState, Action extends NavigationAction> = {
|
|
|
20
20
|
onAction: (action: NavigationAction) => boolean;
|
|
21
21
|
onUnhandledAction: (action: NavigationAction) => void;
|
|
22
22
|
getState: () => State;
|
|
23
|
+
state: State;
|
|
23
24
|
emitter: NavigationEventEmitter<any>;
|
|
24
25
|
router: Router<State, Action>;
|
|
25
|
-
stateRef: React.RefObject<State | null>;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -39,12 +39,29 @@ export function useNavigationHelpers<
|
|
|
39
39
|
onAction,
|
|
40
40
|
onUnhandledAction,
|
|
41
41
|
getState,
|
|
42
|
+
state,
|
|
42
43
|
emitter,
|
|
43
44
|
router,
|
|
44
|
-
stateRef,
|
|
45
45
|
}: Options<State, Action>) {
|
|
46
46
|
const parentNavigationHelpers = React.useContext(NavigationContext);
|
|
47
47
|
|
|
48
|
+
// In some cases (e.g. route names change), internal state might have changed
|
|
49
|
+
// But it hasn't been committed yet, so hasn't propagated to any listeners
|
|
50
|
+
// During this time, we need to return the internal state in `getState`
|
|
51
|
+
// Otherwise it can return an inconsistent state during render in children
|
|
52
|
+
// This may affect hooks like `useNavigationState` that need to read the state during render
|
|
53
|
+
// To avoid this, we use a ref for render phase, and immediately clear it on commit
|
|
54
|
+
// The ref won't be cleared if the render is discarded, e.g. when interrupted
|
|
55
|
+
// So we also track the stored state the render was based on
|
|
56
|
+
// Then ignore the ref once the stored state changes due to another dispatch
|
|
57
|
+
const stateRef = React.useRef<{ state: State; base: State } | null>(null);
|
|
58
|
+
|
|
59
|
+
stateRef.current = { state, base: getState() };
|
|
60
|
+
|
|
61
|
+
React.useInsertionEffect(() => {
|
|
62
|
+
stateRef.current = null;
|
|
63
|
+
});
|
|
64
|
+
|
|
48
65
|
return React.useMemo(() => {
|
|
49
66
|
const dispatch = (op: Action | ((state: State) => Action)) => {
|
|
50
67
|
const action = typeof op === 'function' ? op(getState()) : op;
|
|
@@ -103,16 +120,16 @@ export function useNavigationHelpers<
|
|
|
103
120
|
return parentNavigationHelpers;
|
|
104
121
|
},
|
|
105
122
|
getState: (): State => {
|
|
123
|
+
const current = getState();
|
|
124
|
+
const pending = stateRef.current;
|
|
125
|
+
|
|
106
126
|
// FIXME: Workaround for when the state is read during render
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Apart from `useSyncExternalStore`, `getState` should never be called during render
|
|
111
|
-
if (stateRef.current != null) {
|
|
112
|
-
return stateRef.current;
|
|
127
|
+
// Apart from subscriptions, `getState` should never be called during render
|
|
128
|
+
if (pending != null && pending.base === current) {
|
|
129
|
+
return pending.state;
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
return
|
|
132
|
+
return current;
|
|
116
133
|
},
|
|
117
134
|
} as NavigationHelpers<ParamListBase, EventMap> & ActionHelpers;
|
|
118
135
|
|
package/src/useOnAction.tsx
CHANGED
|
@@ -60,7 +60,7 @@ export function useOnAction<State extends NavigationState>({
|
|
|
60
60
|
const routerConfigOptionsRef =
|
|
61
61
|
React.useRef<RouterConfigOptions>(routerConfigOptions);
|
|
62
62
|
|
|
63
|
-
React.
|
|
63
|
+
React.useInsertionEffect(() => {
|
|
64
64
|
routerConfigOptionsRef.current = routerConfigOptions;
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -31,8 +31,11 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
|
|
|
31
31
|
}
|
|
32
32
|
}, [navigation, onOptionsChange]);
|
|
33
33
|
|
|
34
|
-
React.
|
|
34
|
+
React.useInsertionEffect(() => {
|
|
35
35
|
optionsRef.current = options;
|
|
36
|
+
}, [options]);
|
|
37
|
+
|
|
38
|
+
React.useEffect(() => {
|
|
36
39
|
optionsChangeListener();
|
|
37
40
|
|
|
38
41
|
return navigation?.addListener('focus', optionsChangeListener);
|
package/src/usePreventRemove.tsx
CHANGED
|
@@ -23,15 +23,24 @@ export function usePreventRemove(
|
|
|
23
23
|
const navigation = useNavigation();
|
|
24
24
|
const { key: routeKey } = useRoute();
|
|
25
25
|
|
|
26
|
-
const { setPreventRemove } = usePreventRemoveContext();
|
|
26
|
+
const { setPreventRemove, notifyPreventRemove } = usePreventRemoveContext();
|
|
27
27
|
|
|
28
|
-
React.
|
|
28
|
+
React.useInsertionEffect(() => {
|
|
29
29
|
setPreventRemove(id, routeKey, preventRemove);
|
|
30
|
+
|
|
30
31
|
return () => {
|
|
31
32
|
setPreventRemove(id, routeKey, false);
|
|
32
33
|
};
|
|
33
34
|
}, [setPreventRemove, id, routeKey, preventRemove]);
|
|
34
35
|
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
notifyPreventRemove();
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
notifyPreventRemove();
|
|
41
|
+
};
|
|
42
|
+
}, [id, routeKey, preventRemove, notifyPreventRemove]);
|
|
43
|
+
|
|
35
44
|
const beforeRemoveListener = useLatestCallback<
|
|
36
45
|
EventListenerCallback<EventMapCore<any>, 'beforeRemove'>
|
|
37
46
|
>((e) => {
|
package/src/useRouteCache.tsx
CHANGED
|
@@ -4,8 +4,6 @@ import * as React from 'react';
|
|
|
4
4
|
import { isRecordEqual } from './isRecordEqual';
|
|
5
5
|
import type { RouteProp } from './types';
|
|
6
6
|
|
|
7
|
-
type RouteCache = Map<string, RouteProp<ParamListBase>>;
|
|
8
|
-
|
|
9
7
|
/**
|
|
10
8
|
* Utilities such as `getFocusedRouteNameFromRoute` need to access state.
|
|
11
9
|
* So we need a way to suppress the warning for those use cases.
|
|
@@ -21,9 +19,12 @@ export function useRouteCache<State extends NavigationState>(
|
|
|
21
19
|
routes: State['routes']
|
|
22
20
|
) {
|
|
23
21
|
// Cache object which holds route objects for each screen
|
|
24
|
-
const cache = React.useMemo(
|
|
22
|
+
const cache = React.useMemo(
|
|
23
|
+
() => ({ current: new Map<string, RouteProp<ParamListBase>>() }),
|
|
24
|
+
[]
|
|
25
|
+
);
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
const next = routes.reduce((acc, route) => {
|
|
27
28
|
const previous = cache.current.get(route.key);
|
|
28
29
|
const { state, ...routeWithoutState } = route;
|
|
29
30
|
|
|
@@ -53,16 +54,23 @@ export function useRouteCache<State extends NavigationState>(
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
// @ts-expect-error: this isn't in type definitions coz we want this private
|
|
58
|
+
if (proxy[CHILD_STATE] !== state) {
|
|
59
|
+
Object.defineProperty(proxy, CHILD_STATE, {
|
|
60
|
+
enumerable: false,
|
|
61
|
+
configurable: true,
|
|
62
|
+
value: state,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
61
65
|
|
|
62
66
|
acc.set(route.key, proxy);
|
|
63
67
|
|
|
64
68
|
return acc;
|
|
65
|
-
}, new Map()
|
|
69
|
+
}, new Map<string, RouteProp<ParamListBase>>());
|
|
70
|
+
|
|
71
|
+
React.useInsertionEffect(() => {
|
|
72
|
+
cache.current = next;
|
|
73
|
+
});
|
|
66
74
|
|
|
67
|
-
return Array.from(
|
|
75
|
+
return Array.from(next.values());
|
|
68
76
|
}
|