@react-navigation/core 8.0.0-alpha.26 → 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 (84) 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/useFocusEvents.js +6 -2
  25. package/lib/module/useFocusEvents.js.map +1 -1
  26. package/lib/module/useLazyValue.js.map +1 -1
  27. package/lib/module/useNavigationBuilder.js +8 -6
  28. package/lib/module/useNavigationBuilder.js.map +1 -1
  29. package/lib/module/useOnAction.js +6 -16
  30. package/lib/module/useOnAction.js.map +1 -1
  31. package/lib/module/useOnPreventRemove.js +1 -3
  32. package/lib/module/useOnPreventRemove.js.map +1 -1
  33. package/lib/module/useRouteCache.js +1 -3
  34. package/lib/module/useRouteCache.js.map +1 -1
  35. package/lib/module/useSyncState.js +3 -2
  36. package/lib/module/useSyncState.js.map +1 -1
  37. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  38. package/lib/typescript/src/ConsumedParamsContext.d.ts +1 -6
  39. package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -1
  40. package/lib/typescript/src/DataLoading.d.ts +1 -0
  41. package/lib/typescript/src/DataLoading.d.ts.map +1 -1
  42. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  43. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  44. package/lib/typescript/src/StaticNavigation.d.ts +39 -30
  45. package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
  46. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  47. package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
  48. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  49. package/lib/typescript/src/index.d.ts +2 -1
  50. package/lib/typescript/src/index.d.ts.map +1 -1
  51. package/lib/typescript/src/types.d.ts +2 -2
  52. package/lib/typescript/src/types.d.ts.map +1 -1
  53. package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
  54. package/lib/typescript/src/useLazyValue.d.ts.map +1 -1
  55. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  56. package/lib/typescript/src/useOnAction.d.ts.map +1 -1
  57. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  58. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  59. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  60. package/lib/typescript/src/utilities.d.ts +10 -0
  61. package/lib/typescript/src/utilities.d.ts.map +1 -1
  62. package/package.json +5 -5
  63. package/src/BaseNavigationContainer.tsx +13 -7
  64. package/src/ConsumedParamsContext.tsx +1 -6
  65. package/src/DataLoading.tsx +68 -4
  66. package/src/NavigationIndependentTree.tsx +8 -5
  67. package/src/SceneView.tsx +18 -52
  68. package/src/StaticNavigation.tsx +88 -64
  69. package/src/findFocusedRoute.tsx +1 -1
  70. package/src/getActionFromState.tsx +7 -3
  71. package/src/getPathFromState.tsx +59 -32
  72. package/src/getPatternParts.tsx +15 -2
  73. package/src/getStateFromPath.tsx +77 -17
  74. package/src/index.tsx +5 -0
  75. package/src/types.tsx +2 -2
  76. package/src/useDescriptors.tsx +1 -1
  77. package/src/useFocusEvents.tsx +7 -5
  78. package/src/useLazyValue.tsx +2 -2
  79. package/src/useNavigationBuilder.tsx +12 -7
  80. package/src/useOnAction.tsx +11 -40
  81. package/src/useOnPreventRemove.tsx +4 -3
  82. package/src/useRouteCache.tsx +1 -4
  83. package/src/useSyncState.tsx +3 -2
  84. package/src/utilities.tsx +19 -0
package/src/SceneView.tsx CHANGED
@@ -6,7 +6,6 @@ import type {
6
6
  } from '@react-navigation/routers';
7
7
  import * as React from 'react';
8
8
 
9
- import { ConsumedParamsContext } from './ConsumedParamsContext';
10
9
  import { EnsureSingleNavigator } from './EnsureSingleNavigator';
11
10
  import { isArrayEqual } from './isArrayEqual';
12
11
  import {
@@ -105,37 +104,6 @@ export function SceneView<
105
104
 
106
105
  const getIsInitial = React.useCallback(() => isInitialRef.current, []);
107
106
 
108
- const [consumedParams, setConsumedParams] = React.useState<WeakMap<
109
- object,
110
- true
111
- > | null>(null);
112
-
113
- const consumedParamsContext = React.useMemo(
114
- () => ({
115
- isConsumed: (params: object) => {
116
- if (consumedParams) {
117
- return consumedParams.has(params);
118
- }
119
-
120
- return false;
121
- },
122
- setConsumed: (params: object) => {
123
- setConsumedParams((prev) => {
124
- if (prev && prev.has(params)) {
125
- return prev;
126
- }
127
-
128
- const map = new WeakMap<object, true>();
129
-
130
- map.set(params, true);
131
-
132
- return map;
133
- });
134
- },
135
- }),
136
- [consumedParams]
137
- );
138
-
139
107
  const parentFocusedRouteState = React.use(NavigationFocusedRouteStateContext);
140
108
 
141
109
  const focusedRouteState = React.useMemo(() => {
@@ -205,25 +173,23 @@ export function SceneView<
205
173
  : screen.component;
206
174
 
207
175
  return (
208
- <ConsumedParamsContext.Provider value={consumedParamsContext}>
209
- <NavigationStateContext.Provider value={context}>
210
- <NavigationFocusedRouteStateContext.Provider value={focusedRouteState}>
211
- <EnsureSingleNavigator>
212
- <StaticContainer
213
- name={screen.name}
214
- render={ScreenComponent || screen.children}
215
- navigation={navigation}
216
- route={route}
217
- >
218
- {ScreenComponent !== undefined ? (
219
- <ScreenComponent navigation={navigation} route={route} />
220
- ) : screen.children !== undefined ? (
221
- screen.children({ navigation, route })
222
- ) : null}
223
- </StaticContainer>
224
- </EnsureSingleNavigator>
225
- </NavigationFocusedRouteStateContext.Provider>
226
- </NavigationStateContext.Provider>
227
- </ConsumedParamsContext.Provider>
176
+ <NavigationStateContext.Provider value={context}>
177
+ <NavigationFocusedRouteStateContext.Provider value={focusedRouteState}>
178
+ <EnsureSingleNavigator>
179
+ <StaticContainer
180
+ name={screen.name}
181
+ render={ScreenComponent || screen.children}
182
+ navigation={navigation}
183
+ route={route}
184
+ >
185
+ {ScreenComponent !== undefined ? (
186
+ <ScreenComponent navigation={navigation} route={route} />
187
+ ) : screen.children !== undefined ? (
188
+ screen.children({ navigation, route })
189
+ ) : null}
190
+ </StaticContainer>
191
+ </EnsureSingleNavigator>
192
+ </NavigationFocusedRouteStateContext.Provider>
193
+ </NavigationStateContext.Provider>
228
194
  );
229
195
  }
@@ -32,7 +32,9 @@ import type {
32
32
  HasArguments,
33
33
  InferParamsFromLinking,
34
34
  KeysOf,
35
+ NoExcessCallbackReturn,
35
36
  NoExcessObject,
37
+ NoExcessValue,
36
38
  NotUndefinedObject,
37
39
  StandardSchemaV1,
38
40
  UnionToIntersection,
@@ -53,19 +55,23 @@ type ParamsForScreenComponent<T> = [T] extends [(...args: any[]) => any]
53
55
  : undefined
54
56
  : ScreenComponentParams<T>;
55
57
 
56
- // If every nested route's params include `undefined`, the nested navigator
57
- // itself can be omitted. Otherwise, require `NavigatorScreenParams`.
58
+ // If the initial route's params include `undefined`, the nested navigator itself can be optional.
59
+ // If no initial route is specified, check if every nested route's params include `undefined`.
60
+ // Otherwise the nested navigator needs to be required.
58
61
  type ParamsForNestedNavigator<
59
62
  T extends { config: any },
60
63
  ParamList extends {} = StaticParamList<T>,
61
64
  Params = NavigatorScreenParams<ParamList>,
65
+ RouteNames extends keyof ParamList = T['config'] extends {
66
+ initialRouteName: infer RouteName extends keyof ParamList;
67
+ }
68
+ ? RouteName
69
+ : keyof ParamList,
62
70
  > = {
63
- // Map each route to `never` when params are optional, or name when params are required
64
- // If every route has optional params, the resulting union is `never`
65
- [RouteName in keyof ParamList]-?: undefined extends ParamList[RouteName]
71
+ [RouteName in RouteNames]-?: undefined extends ParamList[RouteName]
66
72
  ? never
67
73
  : RouteName;
68
- }[keyof ParamList] extends never
74
+ }[RouteNames] extends never
69
75
  ? Params | undefined
70
76
  : Params;
71
77
 
@@ -132,6 +138,8 @@ type ParamListForScreens<Screens> = {
132
138
  any,
133
139
  any,
134
140
  any,
141
+ any,
142
+ any,
135
143
  any
136
144
  >
137
145
  ? ParamsForConfig<Linking, Screen>
@@ -146,22 +154,22 @@ type ParamListForGroups<Groups> = Groups extends {
146
154
  ? ParamListForScreens<UnionToIntersection<Screens>>
147
155
  : {};
148
156
 
149
- type StaticRoute<Params, P = AnyToUnknown<Params>> = Readonly<
150
- Omit<Route<string>, 'params'> &
151
- (undefined extends Params
152
- ? {
153
- /**
154
- * Params for this route
155
- */
156
- params?: P extends object ? Readonly<P> : P;
157
- }
158
- : {
159
- /**
160
- * Params for this route
161
- */
162
- params: Readonly<P>;
163
- })
164
- >;
157
+ type StaticRouteBase = Readonly<Omit<Route<string>, 'params'>>;
158
+
159
+ type StaticRoute<Params, P = AnyToUnknown<Params>> = StaticRouteBase &
160
+ (undefined extends Params
161
+ ? {
162
+ /**
163
+ * Params for this route
164
+ */
165
+ readonly params?: P extends object ? Readonly<P> : P;
166
+ }
167
+ : {
168
+ /**
169
+ * Params for this route
170
+ */
171
+ readonly params: Readonly<P>;
172
+ });
165
173
 
166
174
  type StaticScreenConfigLinkingAlias = {
167
175
  /**
@@ -193,7 +201,7 @@ type StaticScreenConfigLinkingAlias = {
193
201
  >;
194
202
  /**
195
203
  * An object mapping the param name to a function which converts the param value to a string.
196
- * By default, all params are converted to strings using `String(value)`.
204
+ * By default, arrays and null are preserved for query params, and other values are converted using `String(value)`.
197
205
  *
198
206
  * @example
199
207
  * ```js
@@ -246,15 +254,17 @@ export type StaticScreenConfigScreen =
246
254
  | React.ComponentType<any>
247
255
  | StaticNavigation<any>;
248
256
 
249
- export type StaticScreenConfig<
250
- Linking extends StaticScreenConfigLinking,
251
- Screen,
252
- State extends NavigationState,
253
- ScreenOptions extends {},
254
- EventMap extends EventMapBase,
255
- Navigation,
256
- Params = ParamsForConfig<Linking, Screen>,
257
- > = {
257
+ export interface StaticScreenConfig<
258
+ out Linking extends StaticScreenConfigLinking,
259
+ out Screen,
260
+ in out State extends NavigationState,
261
+ in out ScreenOptions extends {},
262
+ in out EventMap extends EventMapBase,
263
+ in out Navigation,
264
+ in out Params = ParamsForConfig<Linking, Screen>,
265
+ in out ProvidedOptions extends {} = {},
266
+ in out ProvidedListeners extends {} = {},
267
+ > {
258
268
  /**
259
269
  * Static navigation config or Component to render for the screen.
260
270
  */
@@ -291,7 +301,7 @@ export type StaticScreenConfig<
291
301
  route: StaticRoute<Params>;
292
302
  navigation: Navigation;
293
303
  theme: Theme;
294
- }) => ScreenOptions);
304
+ }) => NoExcessCallbackReturn<ProvidedOptions, ScreenOptions>);
295
305
 
296
306
  /**
297
307
  * Event listeners for this screen.
@@ -310,7 +320,10 @@ export type StaticScreenConfig<
310
320
  | ((props: {
311
321
  route: StaticRoute<Params>;
312
322
  navigation: Navigation;
313
- }) => ScreenListeners<State, EventMap>);
323
+ }) => NoExcessCallbackReturn<
324
+ ProvidedListeners,
325
+ ScreenListeners<State, EventMap>
326
+ >);
314
327
 
315
328
  /**
316
329
  * Layout for this screen.
@@ -392,14 +405,13 @@ export type StaticScreenConfig<
392
405
  name: string;
393
406
  params: AnyToUnknown<Params>;
394
407
  }) => Promise<void>;
395
- };
408
+ }
396
409
 
397
410
  export type StaticScreenFactory<in out Bag extends NavigatorTypeBagBase> = <
398
411
  const Linking extends StaticScreenConfigLinking,
399
412
  const Screen extends StaticScreenConfigScreen,
400
- const ProvidedOptions extends
401
- | Bag['ScreenOptions']
402
- | ((...args: never[]) => unknown) = Bag['ScreenOptions'],
413
+ ProvidedOptions extends {} = {},
414
+ ProvidedListeners extends {} = {},
403
415
  >(
404
416
  config: StaticScreenConfig<
405
417
  Linking,
@@ -407,11 +419,11 @@ export type StaticScreenFactory<in out Bag extends NavigatorTypeBagBase> = <
407
419
  Bag['State'],
408
420
  Bag['ScreenOptions'],
409
421
  Bag['EventMap'],
410
- Bag['NavigationList'][keyof Bag['ParamList']]
411
- > & {
412
- options?: ProvidedOptions &
413
- NoExcessOptions<ProvidedOptions, Bag['ScreenOptions']>;
414
- }
422
+ Bag['NavigationList'][keyof Bag['ParamList']],
423
+ ParamsForConfig<Linking, Screen>,
424
+ ProvidedOptions,
425
+ ProvidedListeners
426
+ >
415
427
  ) => StaticScreenConfig<
416
428
  Linking,
417
429
  Screen,
@@ -446,7 +458,9 @@ type StaticConfigScreens<
446
458
  ScreenOptions,
447
459
  EventMap,
448
460
  NavigationList[RouteName],
449
- any
461
+ any,
462
+ {},
463
+ {}
450
464
  >;
451
465
  };
452
466
 
@@ -529,23 +543,26 @@ export type StaticConfig<
529
543
  }
530
544
  );
531
545
 
532
- type NoExcessOptions<Provided, Allowed extends {}> = Provided extends (
533
- ...args: never[]
534
- ) => infer Return
535
- ? (...args: never[]) => Allowed & NoExcessObject<Return, Allowed>
536
- : NoExcessObject<Provided, Allowed>;
537
-
538
- type NoExcessScreen<Screen, ScreenOptions extends {}> = Screen extends {
539
- screen: unknown;
540
- options: unknown;
541
- }
542
- ? { options: NoExcessOptions<Screen['options'], ScreenOptions> }
543
- : unknown;
546
+ type NoExcessScreen<
547
+ Screen,
548
+ ScreenOptions extends {},
549
+ Listeners extends {},
550
+ > = (Screen extends { screen: unknown; options: unknown }
551
+ ? { options: NoExcessValue<Screen['options'], ScreenOptions> }
552
+ : unknown) &
553
+ (Screen extends { screen: unknown; listeners: unknown }
554
+ ? { listeners: NoExcessValue<Screen['listeners'], Listeners> }
555
+ : unknown);
544
556
 
545
- type NoExcessScreens<Screens, ScreenOptions extends {}> = {
557
+ type NoExcessScreens<
558
+ Screens,
559
+ ScreenOptions extends {},
560
+ Listeners extends {},
561
+ > = {
546
562
  [RouteName in keyof Screens]: NoExcessScreen<
547
563
  Screens[RouteName],
548
- ScreenOptions
564
+ ScreenOptions,
565
+ Listeners
549
566
  >;
550
567
  };
551
568
 
@@ -553,6 +570,7 @@ type NoExcessGroups<
553
570
  Groups,
554
571
  Bag extends NavigatorTypeBagBase,
555
572
  ScreenOptions extends {},
573
+ Listeners extends {},
556
574
  > = {
557
575
  [GroupName in keyof Groups]: NoExcessObject<
558
576
  Groups[GroupName],
@@ -564,7 +582,7 @@ type NoExcessGroups<
564
582
  NavigationListBase<ParamListBase>
565
583
  >
566
584
  > &
567
- NoExcessStaticConfig<Groups[GroupName], Bag, ScreenOptions>;
585
+ NoExcessStaticConfig<Groups[GroupName], Bag, ScreenOptions, Listeners>;
568
586
  };
569
587
 
570
588
  // Each property is probed with `Config extends { key: infer X }` and the results are intersected.
@@ -574,19 +592,25 @@ export type NoExcessStaticConfig<
574
592
  Config,
575
593
  Bag extends NavigatorTypeBagBase,
576
594
  ScreenOptions extends {} = Bag['ScreenOptions'],
595
+ Listeners extends {} = ScreenListeners<Bag['State'], Bag['EventMap']>,
577
596
  > = (Config extends { screenOptions: infer ProvidedOptions }
578
597
  ? {
579
- screenOptions: NoExcessOptions<ProvidedOptions, ScreenOptions>;
598
+ screenOptions: NoExcessValue<ProvidedOptions, ScreenOptions>;
580
599
  }
581
600
  : unknown) &
601
+ (Config extends { screenListeners: infer ProvidedListeners }
602
+ ? {
603
+ screenListeners: NoExcessValue<ProvidedListeners, Listeners>;
604
+ }
605
+ : unknown) &
582
606
  (Config extends { screens: infer Screens }
583
607
  ? {
584
- screens: NoExcessScreens<Screens, ScreenOptions>;
608
+ screens: NoExcessScreens<Screens, ScreenOptions, Listeners>;
585
609
  }
586
610
  : unknown) &
587
611
  (Config extends { groups: infer Groups }
588
612
  ? {
589
- groups: NoExcessGroups<Groups, Bag, ScreenOptions>;
613
+ groups: NoExcessGroups<Groups, Bag, ScreenOptions, Listeners>;
590
614
  }
591
615
  : unknown);
592
616
 
@@ -1138,8 +1162,8 @@ export function createPathConfigForStaticNavigation<ParamList extends {}>(
1138
1162
 
1139
1163
  screenConfig.path = normalizePath(
1140
1164
  key
1141
- .replace(/([A-Z]+)/g, '-$1')
1142
- .replace(/^-/, '')
1165
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
1166
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
1143
1167
  .toLowerCase()
1144
1168
  );
1145
1169
  }
@@ -14,7 +14,7 @@ export function findFocusedRoute(state: InitialState): Result {
14
14
 
15
15
  while (current != null) {
16
16
  const route: InitialState['routes'][number] | undefined =
17
- current.routes[current.index ?? 0];
17
+ current.routes[current.index ?? current.routes.length - 1];
18
18
 
19
19
  if (route?.state == null) {
20
20
  return route;
@@ -195,11 +195,16 @@ export function getActionFromState(
195
195
  };
196
196
  }
197
197
 
198
- const createNormalizedConfigItem = (config: PathConfig<object> | string) =>
198
+ const createNormalizedConfigItem = (
199
+ config: PathConfig<object> | string
200
+ ): ConfigItem =>
199
201
  typeof config === 'object' && config != null
200
202
  ? {
201
203
  initialRouteName:
202
- 'initialRouteName' in config ? config.initialRouteName : undefined,
204
+ 'initialRouteName' in config &&
205
+ typeof config.initialRouteName === 'string'
206
+ ? config.initialRouteName
207
+ : undefined,
203
208
  screens:
204
209
  'screens' in config && config.screens != null
205
210
  ? createNormalizedConfigs(config.screens)
@@ -209,7 +214,6 @@ const createNormalizedConfigItem = (config: PathConfig<object> | string) =>
209
214
 
210
215
  const createNormalizedConfigs = (options: PathConfigMap<object>) =>
211
216
  Object.entries(options).reduce<Record<string, ConfigItem>>((acc, [k, v]) => {
212
- // @ts-expect-error -we can't properly type this
213
217
  acc[k] = createNormalizedConfigItem(v);
214
218
  return acc;
215
219
  }, {});
@@ -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) {