@react-navigation/core 8.0.0-alpha.25 → 8.0.0-alpha.27

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 (85) hide show
  1. package/lib/module/BaseNavigationContainer.js +12 -6
  2. package/lib/module/BaseNavigationContainer.js.map +1 -1
  3. package/lib/module/ConsumedParamsContext.js.map +1 -1
  4. package/lib/module/DataLoading.js +29 -4
  5. package/lib/module/DataLoading.js.map +1 -1
  6. package/lib/module/NavigationIndependentTree.js +8 -4
  7. package/lib/module/NavigationIndependentTree.js.map +1 -1
  8. package/lib/module/SceneView.js +16 -39
  9. package/lib/module/SceneView.js.map +1 -1
  10. package/lib/module/StaticNavigation.js +4 -3
  11. package/lib/module/StaticNavigation.js.map +1 -1
  12. package/lib/module/findFocusedRoute.js +1 -1
  13. package/lib/module/findFocusedRoute.js.map +1 -1
  14. package/lib/module/getActionFromState.js +1 -2
  15. package/lib/module/getActionFromState.js.map +1 -1
  16. package/lib/module/getPathFromState.js +41 -26
  17. package/lib/module/getPathFromState.js.map +1 -1
  18. package/lib/module/getPatternParts.js +15 -2
  19. package/lib/module/getPatternParts.js.map +1 -1
  20. package/lib/module/getStateFromPath.js +45 -19
  21. package/lib/module/getStateFromPath.js.map +1 -1
  22. package/lib/module/index.js +1 -0
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/types.js.map +1 -1
  25. package/lib/module/useFocusEvents.js +6 -2
  26. package/lib/module/useFocusEvents.js.map +1 -1
  27. package/lib/module/useLazyValue.js.map +1 -1
  28. package/lib/module/useNavigationBuilder.js +8 -6
  29. package/lib/module/useNavigationBuilder.js.map +1 -1
  30. package/lib/module/useOnAction.js +6 -16
  31. package/lib/module/useOnAction.js.map +1 -1
  32. package/lib/module/useOnPreventRemove.js +1 -3
  33. package/lib/module/useOnPreventRemove.js.map +1 -1
  34. package/lib/module/useRouteCache.js +1 -3
  35. package/lib/module/useRouteCache.js.map +1 -1
  36. package/lib/module/useSyncState.js +3 -2
  37. package/lib/module/useSyncState.js.map +1 -1
  38. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  39. package/lib/typescript/src/ConsumedParamsContext.d.ts +1 -6
  40. package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -1
  41. package/lib/typescript/src/DataLoading.d.ts +1 -0
  42. package/lib/typescript/src/DataLoading.d.ts.map +1 -1
  43. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  44. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  45. package/lib/typescript/src/StaticNavigation.d.ts +39 -30
  46. package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
  47. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  48. package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
  49. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  50. package/lib/typescript/src/index.d.ts +2 -1
  51. package/lib/typescript/src/index.d.ts.map +1 -1
  52. package/lib/typescript/src/types.d.ts +10 -7
  53. package/lib/typescript/src/types.d.ts.map +1 -1
  54. package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
  55. package/lib/typescript/src/useLazyValue.d.ts.map +1 -1
  56. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  57. package/lib/typescript/src/useOnAction.d.ts.map +1 -1
  58. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  59. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  60. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  61. package/lib/typescript/src/utilities.d.ts +10 -0
  62. package/lib/typescript/src/utilities.d.ts.map +1 -1
  63. package/package.json +5 -5
  64. package/src/BaseNavigationContainer.tsx +13 -7
  65. package/src/ConsumedParamsContext.tsx +1 -6
  66. package/src/DataLoading.tsx +68 -4
  67. package/src/NavigationIndependentTree.tsx +8 -5
  68. package/src/SceneView.tsx +18 -52
  69. package/src/StaticNavigation.tsx +88 -64
  70. package/src/findFocusedRoute.tsx +1 -1
  71. package/src/getActionFromState.tsx +7 -3
  72. package/src/getPathFromState.tsx +59 -32
  73. package/src/getPatternParts.tsx +15 -2
  74. package/src/getStateFromPath.tsx +77 -17
  75. package/src/index.tsx +5 -0
  76. package/src/types.tsx +14 -7
  77. package/src/useDescriptors.tsx +1 -1
  78. package/src/useFocusEvents.tsx +7 -5
  79. package/src/useLazyValue.tsx +2 -2
  80. package/src/useNavigationBuilder.tsx +12 -7
  81. package/src/useOnAction.tsx +11 -40
  82. package/src/useOnPreventRemove.tsx +4 -3
  83. package/src/useRouteCache.tsx +1 -4
  84. package/src/useSyncState.tsx +3 -2
  85. package/src/utilities.tsx +19 -0
@@ -16,12 +16,22 @@ type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
16
16
 
17
17
  type StringifyConfig = Record<string, ((value: unknown) => string) | undefined>;
18
18
 
19
+ type SerializedParamValue = string | string[] | null;
20
+
19
21
  type ConfigItem = {
20
22
  parts?: PatternPart[] | undefined;
23
+ ownParts: PatternPart[];
21
24
  stringify?: StringifyConfig | undefined;
22
25
  screens?: Record<string, ConfigItem> | undefined;
23
26
  };
24
27
 
28
+ const serializeParamValue = (value: unknown): SerializedParamValue =>
29
+ value === null
30
+ ? null
31
+ : Array.isArray(value)
32
+ ? value.map(String)
33
+ : String(value);
34
+
25
35
  const encodePathParam = (value: string) => {
26
36
  let result = '';
27
37
 
@@ -170,8 +180,6 @@ export function getPathFromState<ParamList extends {}>(
170
180
  let path = '/';
171
181
  let current: State | undefined = state;
172
182
 
173
- const allParams: Record<string, string> = {};
174
-
175
183
  while (current) {
176
184
  let index: number = typeof current.index === 'number' ? current.index : 0;
177
185
 
@@ -186,7 +194,11 @@ export function getPathFromState<ParamList extends {}>(
186
194
 
187
195
  let parts: PatternPart[] | undefined;
188
196
 
189
- let focusedParams: Record<string, string> | undefined;
197
+ // Values for each pattern part object, so the same param name
198
+ // at different nesting levels gets the value from its own level
199
+ const partValues = new Map<PatternPart, SerializedParamValue | undefined>();
200
+
201
+ let focusedParams: Record<string, SerializedParamValue> | undefined;
190
202
  let currentOptions = configs;
191
203
 
192
204
  // Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
@@ -203,12 +215,14 @@ export function getPathFromState<ParamList extends {}>(
203
215
 
204
216
  parts = config.parts;
205
217
 
218
+ const ownParts = config.ownParts;
219
+
206
220
  nestedRouteNames.push(route.name);
207
221
 
208
222
  if (route.params) {
209
223
  const options = config;
210
224
  const params = route.params as Record<string, unknown>;
211
- const currentParams: Record<string, string> = {};
225
+ const currentParams: Record<string, SerializedParamValue> = {};
212
226
 
213
227
  for (const key in params) {
214
228
  const value = params[key];
@@ -216,7 +230,7 @@ export function getPathFromState<ParamList extends {}>(
216
230
  if (value === undefined) {
217
231
  let optional = false;
218
232
 
219
- for (const part of options.parts ?? []) {
233
+ for (const part of ownParts) {
220
234
  if (part.param === key) {
221
235
  optional = part.optional === true;
222
236
  break;
@@ -228,39 +242,52 @@ export function getPathFromState<ParamList extends {}>(
228
242
  }
229
243
  }
230
244
 
231
- const stringify = options.stringify?.[key] ?? String;
245
+ const stringify = options.stringify?.[key];
232
246
 
233
- currentParams[key] = stringify(value);
247
+ currentParams[key] = stringify
248
+ ? stringify(value)
249
+ : serializeParamValue(value);
234
250
  }
235
251
 
236
- if (parts?.length) {
237
- Object.assign(allParams, currentParams);
252
+ const claimedParams = new Set<string>();
253
+
254
+ for (const part of ownParts) {
255
+ if (part.param && part.param in currentParams) {
256
+ const value = currentParams[part.param];
257
+
258
+ if (value !== undefined) {
259
+ partValues.set(part, value);
260
+ claimedParams.add(part.param);
261
+ }
262
+ }
238
263
  }
239
264
 
240
265
  if (focusedRoute === route) {
241
266
  // If this is the focused route, keep the params for later use
242
267
  // We save it here since it's been stringified already
268
+ // Params claimed by the pattern shouldn't be repeated in the query string
243
269
  focusedParams = {};
244
270
 
245
271
  for (const key in currentParams) {
246
- let inPattern = false;
247
-
248
- for (const part of parts ?? []) {
249
- if (part.param === key) {
250
- inPattern = true;
251
- break;
252
- }
253
- }
254
-
255
272
  const value = currentParams[key];
256
273
 
257
- if (!inPattern && value !== undefined && value !== 'undefined') {
274
+ if (
275
+ !claimedParams.has(key) &&
276
+ value !== undefined &&
277
+ value !== 'undefined'
278
+ ) {
258
279
  focusedParams[key] = value;
259
280
  }
260
281
  }
261
282
  }
262
283
  }
263
284
 
285
+ for (const part of ownParts) {
286
+ if (part.param && !partValues.has(part)) {
287
+ partValues.set(part, undefined);
288
+ }
289
+ }
290
+
264
291
  // If there is no `screens` property or no nested state, we return pattern
265
292
  const routeState = getRouteState(route, config);
266
293
 
@@ -298,7 +325,9 @@ export function getPathFromState<ParamList extends {}>(
298
325
  if (parts) {
299
326
  let index = 0;
300
327
 
301
- for (const { segment, param, optional } of parts) {
328
+ for (const part of parts) {
329
+ const { segment, param, optional } = part;
330
+
302
331
  if (index > 0) {
303
332
  path += '/';
304
333
  }
@@ -315,7 +344,7 @@ export function getPathFromState<ParamList extends {}>(
315
344
 
316
345
  // If the path has a pattern for a param, put the param in the path
317
346
  if (param) {
318
- const value = allParams[param];
347
+ const value = partValues.get(part);
319
348
 
320
349
  if (value === undefined && optional) {
321
350
  // Optional params without value assigned in route.params should be ignored
@@ -341,7 +370,7 @@ export function getPathFromState<ParamList extends {}>(
341
370
  const params = focusedRoute.params as Record<string, unknown>;
342
371
 
343
372
  for (const key in params) {
344
- const value = String(params[key]);
373
+ const value = serializeParamValue(params[key]);
345
374
 
346
375
  if (value !== 'undefined') {
347
376
  focusedParams[key] = value;
@@ -385,14 +414,13 @@ const createConfigItem = (
385
414
  parentParts?: PatternPart[]
386
415
  ): ConfigItem => {
387
416
  if (typeof config === 'string') {
388
- // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
389
- const parts = getPatternParts(config);
417
+ const ownParts = getPatternParts(config);
390
418
 
391
419
  if (parentParts) {
392
- return { parts: [...parentParts, ...parts] };
420
+ return { parts: [...parentParts, ...ownParts], ownParts };
393
421
  }
394
422
 
395
- return { parts };
423
+ return { parts: ownParts, ownParts };
396
424
  }
397
425
 
398
426
  if (config.exact && config.path === undefined) {
@@ -403,14 +431,12 @@ const createConfigItem = (
403
431
 
404
432
  // If an object is specified as the value (e.g. Foo: { ... }),
405
433
  // It can have `path` property and `screens` prop which has nested configs
434
+ const ownParts = config.path ? getPatternParts(config.path) : [];
406
435
  const parts =
407
436
  config.exact !== true
408
- ? [
409
- ...(parentParts || []),
410
- ...(config.path ? getPatternParts(config.path) : []),
411
- ]
412
- : config.path
413
- ? getPatternParts(config.path)
437
+ ? [...(parentParts || []), ...ownParts]
438
+ : ownParts.length
439
+ ? ownParts
414
440
  : undefined;
415
441
 
416
442
  const screens =
@@ -420,6 +446,7 @@ const createConfigItem = (
420
446
 
421
447
  return {
422
448
  parts,
449
+ ownParts,
423
450
  stringify: config.stringify,
424
451
  screens,
425
452
  };
@@ -15,6 +15,8 @@ export function getPatternParts(path: string): PatternPart[] {
15
15
 
16
16
  let isRegex = false;
17
17
  let isParam = false;
18
+ let isEscaped = false;
19
+ let isInCharClass = false;
18
20
  let regexInnerParens = 0;
19
21
 
20
22
  // One extra iteration to add the last character
@@ -34,7 +36,7 @@ export function getPatternParts(path: string): PatternPart[] {
34
36
  `Encountered ':' in the middle of a segment in path: ${path}`
35
37
  );
36
38
  }
37
- } else if (char === '(') {
39
+ } else if (char === '(' && !isEscaped && !isInCharClass) {
38
40
  if (isParam) {
39
41
  if (isRegex) {
40
42
  // The '(' is part of the regex if we're already inside one
@@ -47,7 +49,7 @@ export function getPatternParts(path: string): PatternPart[] {
47
49
  `Encountered '(' without preceding ':' in path: ${path}`
48
50
  );
49
51
  }
50
- } else if (char === ')') {
52
+ } else if (char === ')' && !isEscaped && !isInCharClass) {
51
53
  if (isParam && isRegex) {
52
54
  if (regexInnerParens) {
53
55
  // The ')' is part of the regex if we're already inside one
@@ -102,6 +104,17 @@ export function getPatternParts(path: string): PatternPart[] {
102
104
  if (isRegex) {
103
105
  current.regex = current.regex || '';
104
106
  current.regex += char;
107
+
108
+ // Track escapes and character classes so parens inside them are not special
109
+ if (isEscaped) {
110
+ isEscaped = false;
111
+ } else if (char === '\\') {
112
+ isEscaped = true;
113
+ } else if (char === '[') {
114
+ isInCharClass = true;
115
+ } else if (char === ']') {
116
+ isInCharClass = false;
117
+ }
105
118
  }
106
119
 
107
120
  if (isParam && !isRegex) {
@@ -39,7 +39,12 @@ type RouteConfig = {
39
39
  regex?: RegExp | undefined;
40
40
  pattern: string;
41
41
  segments: string[];
42
- params: { screen: string; name: string; index: number }[];
42
+ params: {
43
+ screen: string;
44
+ name: string;
45
+ index: number;
46
+ regex?: RegExp | undefined;
47
+ }[];
43
48
  routeNames: string[];
44
49
  parse?: ParseConfig | undefined;
45
50
  explicitParamNames?: Set<string> | undefined;
@@ -69,6 +74,7 @@ type ConfigResources = {
69
74
  configs: RouteConfig[];
70
75
  configsByScreen: Record<string, RouteConfig[]>;
71
76
  configsByPattern: Record<string, RouteConfig[]>;
77
+ prefixRegex?: RegExp | undefined;
72
78
  };
73
79
 
74
80
  const INVALID_SCHEMA_RESULT_ERROR =
@@ -88,6 +94,17 @@ const NESTED_SCREEN_PARAM_NAMES = [
88
94
  'pop',
89
95
  ];
90
96
 
97
+ const getStaticSegmentPattern = (segment: string) =>
98
+ Array.from(segment, (char) => {
99
+ const encoded = encodeURIComponent(char);
100
+ const percentEncoded =
101
+ encoded === char
102
+ ? `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`
103
+ : encoded;
104
+
105
+ return `(?:${escape(char)}|${escape(percentEncoded)})`;
106
+ }).join('');
107
+
91
108
  const getExplicitParamNames = (parse?: ParseConfig) => {
92
109
  const names: string[] = [];
93
110
 
@@ -161,8 +178,13 @@ export function getStateFromPath<ParamList extends {}>(
161
178
  options?: Options<ParamList>,
162
179
  previous?: NavigationState
163
180
  ): ResultState | undefined {
164
- const { initialRoutes, configs, configsByScreen, configsByPattern } =
165
- getConfigResources(options);
181
+ const {
182
+ initialRoutes,
183
+ configs,
184
+ configsByScreen,
185
+ configsByPattern,
186
+ prefixRegex,
187
+ } = getConfigResources(options);
166
188
 
167
189
  const screens = options?.screens;
168
190
 
@@ -183,24 +205,20 @@ export function getStateFromPath<ParamList extends {}>(
183
205
  let remaining = path
184
206
  .replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
185
207
  .replace(/^\//, '') // Remove extra leading slash
186
- .replace(/\?.*$/, ''); // Remove query params which we will handle later
208
+ .replace(/\?.*$/, '') // Remove query params which we will handle later
209
+ .replace(/%[0-9a-f]{2}/gi, (m) => m.toUpperCase()); // Normalize percent-encoding as hex digits are case-insensitive
187
210
 
188
211
  // Make sure there is a trailing slash
189
212
  remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
190
213
 
191
- const prefix = options?.path?.replace(/^\//, ''); // Remove extra leading slash
192
-
193
- if (prefix) {
194
- // Make sure there is a trailing slash
195
- const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
214
+ if (prefixRegex) {
215
+ const prefixMatch = remaining.match(prefixRegex);
196
216
 
197
- // If the path doesn't start with the prefix, it's not a match
198
- if (!remaining.startsWith(normalizedPrefix)) {
217
+ if (prefixMatch == null) {
199
218
  return undefined;
200
219
  }
201
220
 
202
- // Remove the prefix from the path
203
- remaining = remaining.replace(normalizedPrefix, '');
221
+ remaining = remaining.slice(prefixMatch[0].length);
204
222
  }
205
223
 
206
224
  if (screens === undefined) {
@@ -358,6 +376,19 @@ function getConfigResources<ParamList extends {}>(
358
376
  function prepareConfigResources(options?: Options<{}>) {
359
377
  const initialRoutes = getInitialRoutes(options);
360
378
  const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
379
+ const prefix = options?.path?.replace(/^\//, '');
380
+
381
+ let prefixRegex: RegExp | undefined;
382
+
383
+ if (prefix) {
384
+ const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
385
+ const prefixPattern = normalizedPrefix
386
+ .split('/')
387
+ .map(getStaticSegmentPattern)
388
+ .join('/');
389
+
390
+ prefixRegex = new RegExp(`^${prefixPattern}`);
391
+ }
361
392
 
362
393
  const configsByScreen: Record<string, RouteConfig[]> = {};
363
394
  const configsByPattern: Record<string, RouteConfig[]> = {};
@@ -380,6 +411,7 @@ function prepareConfigResources(options?: Options<{}>) {
380
411
  configs,
381
412
  configsByScreen,
382
413
  configsByPattern,
414
+ prefixRegex,
383
415
  };
384
416
  }
385
417
 
@@ -428,7 +460,21 @@ function getSortedNormalizedConfigs(
428
460
  // If 2 patterns are same, move the one with less route names up
429
461
  // This is an error state, so it's only useful for consistent error messages
430
462
  if (isArrayEqual(a.segments, b.segments)) {
431
- return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
463
+ if (
464
+ a.routeNames.length > b.routeNames.length &&
465
+ arrayStartsWith(a.routeNames, b.routeNames)
466
+ ) {
467
+ return -1;
468
+ }
469
+
470
+ if (
471
+ b.routeNames.length > a.routeNames.length &&
472
+ arrayStartsWith(b.routeNames, a.routeNames)
473
+ ) {
474
+ return 1;
475
+ }
476
+
477
+ return a.routeNames.length - b.routeNames.length || a.order - b.order;
432
478
  }
433
479
 
434
480
  // If one of the patterns starts with the other, it's more exhaustive
@@ -585,6 +631,12 @@ const matchAgainstConfig = (
585
631
  return undefined;
586
632
  }
587
633
 
634
+ // Percent-encoded values are matched permissively
635
+ // So validate the decoded value against the custom regex
636
+ if (param.regex && value !== decoded && !param.regex.test(decoded)) {
637
+ return undefined;
638
+ }
639
+
588
640
  const parser = routeConfig.parse?.[param.name];
589
641
 
590
642
  if (!parser) {
@@ -799,20 +851,27 @@ const createConfigItem = (
799
851
  segments.push(part.segment);
800
852
 
801
853
  if (part.param) {
802
- const reg = part.regex || '[^/]+';
854
+ // A custom regex may not match the percent-encoded form of the value
855
+ // So also accept segments containing an encoded character and validate them after decoding
856
+ const reg = part.regex
857
+ ? `(?:${part.regex})|(?=[^/]*%[0-9A-F]{2})[^/]+`
858
+ : '[^/]+';
803
859
 
804
860
  regexString += `(((?<${PARAM_GROUP_PREFIX}${index}>${reg})\\/)${part.optional ? '?' : ''})`;
805
861
  params.push({
806
862
  index,
807
863
  screen: part.screen,
808
864
  name: part.param,
865
+ regex: part.regex ? new RegExp(`^(?:${part.regex})$`) : undefined,
809
866
  });
810
867
 
811
868
  if (part.screen === screen) {
812
869
  pathParamNames.add(part.param);
813
870
  }
871
+ } else if (part.segment === '*') {
872
+ regexString += `.*\\/`;
814
873
  } else {
815
- regexString += `${part.segment === '*' ? '.*' : escape(part.segment)}\\/`;
874
+ regexString += `${getStaticSegmentPattern(part.segment)}\\/`;
816
875
  }
817
876
 
818
877
  index++;
@@ -1083,7 +1142,8 @@ const parseQueryParams = (
1083
1142
  ):
1084
1143
  | { valid: true; params?: Record<string, unknown> | undefined }
1085
1144
  | { valid: false } => {
1086
- const query = path.split('?')[1];
1145
+ const queryIndex = path.indexOf('?');
1146
+ const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
1087
1147
  const params: Record<string, unknown> = query ? queryString.parse(query) : {};
1088
1148
 
1089
1149
  // Path params should always win over same-named query params.
package/src/index.tsx CHANGED
@@ -11,6 +11,10 @@ export { getFocusedRouteNameFromRoute } from './getFocusedRouteNameFromRoute';
11
11
  export { getPathFromState } from './getPathFromState';
12
12
  export { getStateFromPath } from './getStateFromPath';
13
13
  export { NavigationContainerRefContext } from './NavigationContainerRefContext';
14
+ export {
15
+ type FocusedRouteState,
16
+ NavigationFocusedRouteStateContext,
17
+ } from './NavigationFocusedRouteStateContext';
14
18
  export { NavigationHelpersContext } from './NavigationHelpersContext';
15
19
  export { NavigationIndependentTree } from './NavigationIndependentTree';
16
20
  export { NavigationMetaContext } from './NavigationMetaContext';
@@ -61,6 +65,7 @@ export type {
61
65
  NavigatorTypeBagBase,
62
66
  NavigatorTypeBagFor,
63
67
  NestedNavigatorsForParamList,
68
+ ParamListForNavigator,
64
69
  PathConfig,
65
70
  PathConfigMap,
66
71
  RootNavigator,
package/src/types.tsx CHANGED
@@ -93,7 +93,7 @@ export type DefaultNavigatorOptions<
93
93
  ScreenOptions,
94
94
  EventMap
95
95
  >,
96
- RouteProp<ParamList>
96
+ ScreenRouteProp<ParamList>
97
97
  >
98
98
  >;
99
99
  children: React.ReactNode;
@@ -107,7 +107,7 @@ export type DefaultNavigatorOptions<
107
107
  | (
108
108
  | ScreenListeners<State, EventMap>
109
109
  | ((props: {
110
- route: RouteProp<ParamList>;
110
+ route: ScreenRouteProp<ParamList>;
111
111
  navigation: Navigation;
112
112
  }) => ScreenListeners<State, EventMap>)
113
113
  )
@@ -120,7 +120,7 @@ export type DefaultNavigatorOptions<
120
120
  | (
121
121
  | ScreenOptions
122
122
  | ((props: {
123
- route: RouteProp<ParamList>;
123
+ route: ScreenRouteProp<ParamList>;
124
124
  navigation: Navigation;
125
125
  theme: Theme;
126
126
  }) => ScreenOptions)
@@ -298,7 +298,7 @@ type NavigatorLike =
298
298
  getComponent: () => React.ComponentType<any>;
299
299
  };
300
300
 
301
- type ParamListForNavigator<T extends {}> = T extends NavigatorLike
301
+ export type ParamListForNavigator<T extends {}> = T extends NavigatorLike
302
302
  ? T extends PrivateValueStore<[infer ParamList extends {}, any, any, any]>
303
303
  ? ParamList
304
304
  : T
@@ -569,6 +569,13 @@ export type RouteProp<
569
569
  in out RouteName extends keyof ParamList = KeyOf<ParamList>,
570
570
  > = Route<Extract<RouteName, string>, ParamList[RouteName]>;
571
571
 
572
+ type ScreenRouteProp<
573
+ ParamList extends ParamListBase,
574
+ RouteName extends keyof ParamList = KeyOf<ParamList>,
575
+ > = {
576
+ [Name in Extract<RouteName, string>]: RouteProp<ParamList, Name>;
577
+ }[Extract<RouteName, string>];
578
+
572
579
  export type CompositeNavigationProp<
573
580
  A extends NavigationProp<ParamListBase, any, any, any, any>,
574
581
  B extends NavigationProp<ParamListBase, any, any, any, any>,
@@ -662,7 +669,7 @@ export type ScreenLayoutArgs<
662
669
  in out ScreenOptions extends {},
663
670
  in out Navigation,
664
671
  > = {
665
- route: RouteProp<ParamList, RouteName>;
672
+ route: ScreenRouteProp<ParamList, RouteName>;
666
673
  options: ScreenOptions;
667
674
  navigation: Navigation;
668
675
  theme: Theme;
@@ -860,7 +867,7 @@ export type RouteGroupConfig<
860
867
  | (
861
868
  | ScreenOptions
862
869
  | ((props: {
863
- route: RouteProp<ParamList, keyof ParamList>;
870
+ route: ScreenRouteProp<ParamList>;
864
871
  navigation: Navigation;
865
872
  theme: Theme;
866
873
  }) => ScreenOptions)
@@ -1647,7 +1654,7 @@ export type PathConfig<Params> = FlatType<
1647
1654
  path?: string | undefined;
1648
1655
  /**
1649
1656
  * An object mapping the param name to a function which converts the param value to a string.
1650
- * By default, all params are converted to strings using `String(value)`.
1657
+ * By default, arrays and null are preserved for query params, and other values are converted using `String(value)`.
1651
1658
  * Keys are constrained to valid param names when Params type is provided.
1652
1659
  *
1653
1660
  * @example
@@ -315,7 +315,7 @@ export function useDescriptors<
315
315
  render() {
316
316
  return element;
317
317
  },
318
- options: customOptions as ScreenOptions,
318
+ options: customOptions,
319
319
  };
320
320
 
321
321
  return acc;
@@ -51,7 +51,12 @@ export function useFocusEvents<State extends NavigationState>({
51
51
  React.useEffect(() => {
52
52
  const lastFocusedKey = lastFocusedKeyRef.current;
53
53
 
54
- lastFocusedKeyRef.current = currentFocusedKey;
54
+ const isFocused = navigation ? navigation.isFocused() : true;
55
+
56
+ if (isFocused) {
57
+ // Remember the current route as focused if the navigator is focused
58
+ lastFocusedKeyRef.current = currentFocusedKey;
59
+ }
55
60
 
56
61
  // We wouldn't have `lastFocusedKey` on initial mount
57
62
  // Fire focus event for the current route on mount if there's no parent navigator
@@ -61,10 +66,7 @@ export function useFocusEvents<State extends NavigationState>({
61
66
 
62
67
  // We should only emit events when the focused key changed and navigator is focused
63
68
  // When navigator is not focused, screens inside shouldn't receive focused status either
64
- if (
65
- lastFocusedKey === currentFocusedKey ||
66
- !(navigation ? navigation.isFocused() : true)
67
- ) {
69
+ if (lastFocusedKey === currentFocusedKey || !isFocused) {
68
70
  return;
69
71
  }
70
72
 
@@ -1,11 +1,11 @@
1
1
  import * as React from 'react';
2
2
 
3
- export function useLazyValue<T>(create: () => T) {
3
+ export function useLazyValue<T>(create: () => T): T {
4
4
  const lazyRef = React.useRef<T>(undefined);
5
5
 
6
6
  if (lazyRef.current === undefined) {
7
7
  lazyRef.current = create();
8
8
  }
9
9
 
10
- return lazyRef.current as T;
10
+ return lazyRef.current;
11
11
  }
@@ -337,7 +337,7 @@ export function useNavigationBuilder<
337
337
 
338
338
  const isNestedParamsConsumed =
339
339
  typeof route?.params === 'object' && route.params != null
340
- ? consumedParams?.isConsumed(route.params)
340
+ ? consumedParams?.has(route.params)
341
341
  : false;
342
342
 
343
343
  const {
@@ -571,7 +571,14 @@ export function useNavigationBuilder<
571
571
  return [stateBeforeInitialization, hydratedState, true, paramsForState];
572
572
  }
573
573
 
574
- return [undefined, hydratedState, false, paramsForState];
574
+ return [
575
+ undefined,
576
+ hydratedState,
577
+ false,
578
+ // Current state is already initialized
579
+ // So params weren't used, and need to be handled later
580
+ isStateInitialized(currentState) ? undefined : paramsForState,
581
+ ];
575
582
  }
576
583
  // We explicitly don't include routeNames, route.params etc. in the dep list
577
584
  // below. We want to avoid forcing a new state to be calculated in those cases
@@ -719,18 +726,16 @@ export function useNavigationBuilder<
719
726
  : nextState;
720
727
  }
721
728
 
722
- const setConsumedParams = consumedParams?.setConsumed;
723
-
724
729
  React.useEffect(() => {
725
730
  if (
726
- setConsumedParams &&
731
+ consumedParams &&
727
732
  didConsumeNestedParams &&
728
733
  typeof route?.params === 'object' &&
729
734
  route.params != null
730
735
  ) {
731
- setConsumedParams(route.params);
736
+ consumedParams.set(route.params, true);
732
737
  }
733
- }, [didConsumeNestedParams, route?.params, setConsumedParams]);
738
+ }, [consumedParams, didConsumeNestedParams, route?.params]);
734
739
 
735
740
  const shouldUpdate = state !== nextState;
736
741