@react-navigation/core 7.21.5 → 7.21.6

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 (87) hide show
  1. package/lib/module/BaseNavigationContainer.js +14 -8
  2. package/lib/module/BaseNavigationContainer.js.map +1 -1
  3. package/lib/module/ConsumedParamsContext.js.map +1 -1
  4. package/lib/module/NavigationIndependentTree.js +8 -4
  5. package/lib/module/NavigationIndependentTree.js.map +1 -1
  6. package/lib/module/SceneView.js +16 -39
  7. package/lib/module/SceneView.js.map +1 -1
  8. package/lib/module/StaticNavigation.js +1 -1
  9. package/lib/module/StaticNavigation.js.map +1 -1
  10. package/lib/module/findFocusedRoute.js +3 -3
  11. package/lib/module/findFocusedRoute.js.map +1 -1
  12. package/lib/module/getActionFromState.js.map +1 -1
  13. package/lib/module/getPathFromState.js +75 -45
  14. package/lib/module/getPathFromState.js.map +1 -1
  15. package/lib/module/getPatternParts.js +15 -2
  16. package/lib/module/getPatternParts.js.map +1 -1
  17. package/lib/module/getStateFromPath.js +47 -21
  18. package/lib/module/getStateFromPath.js.map +1 -1
  19. package/lib/module/getStateFromRouteParams.js +24 -0
  20. package/lib/module/getStateFromRouteParams.js.map +1 -0
  21. package/lib/module/useFocusEvents.js +6 -2
  22. package/lib/module/useFocusEvents.js.map +1 -1
  23. package/lib/module/useLazyValue.js.map +1 -1
  24. package/lib/module/useNavigationBuilder.js +8 -6
  25. package/lib/module/useNavigationBuilder.js.map +1 -1
  26. package/lib/module/useOnPreventRemove.js +1 -3
  27. package/lib/module/useOnPreventRemove.js.map +1 -1
  28. package/lib/module/useRouteCache.js +1 -3
  29. package/lib/module/useRouteCache.js.map +1 -1
  30. package/lib/module/useSyncState.js +3 -2
  31. package/lib/module/useSyncState.js.map +1 -1
  32. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  33. package/lib/typescript/src/ConsumedParamsContext.d.ts +1 -6
  34. package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -1
  35. package/lib/typescript/src/EnsureSingleNavigator.d.ts +1 -1
  36. package/lib/typescript/src/EnsureSingleNavigator.d.ts.map +1 -1
  37. package/lib/typescript/src/NavigationIndependentTree.d.ts +1 -1
  38. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  39. package/lib/typescript/src/NavigationProvider.d.ts +1 -1
  40. package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
  41. package/lib/typescript/src/PreventRemoveProvider.d.ts +1 -1
  42. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  43. package/lib/typescript/src/SceneView.d.ts +2 -1
  44. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  45. package/lib/typescript/src/StaticContainer.d.ts +1 -1
  46. package/lib/typescript/src/StaticContainer.d.ts.map +1 -1
  47. package/lib/typescript/src/findFocusedRoute.d.ts.map +1 -1
  48. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  49. package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
  50. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  51. package/lib/typescript/src/getStateFromRouteParams.d.ts +3 -0
  52. package/lib/typescript/src/getStateFromRouteParams.d.ts.map +1 -0
  53. package/lib/typescript/src/theming/ThemeProvider.d.ts +1 -1
  54. package/lib/typescript/src/theming/ThemeProvider.d.ts.map +1 -1
  55. package/lib/typescript/src/types.d.ts +15 -9
  56. package/lib/typescript/src/types.d.ts.map +1 -1
  57. package/lib/typescript/src/useComponent.d.ts +1 -1
  58. package/lib/typescript/src/useComponent.d.ts.map +1 -1
  59. package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
  60. package/lib/typescript/src/useLazyValue.d.ts.map +1 -1
  61. package/lib/typescript/src/useNavigationBuilder.d.ts +1 -1
  62. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  63. package/lib/typescript/src/useNavigationState.d.ts +1 -1
  64. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  65. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  66. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  67. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  68. package/package.json +7 -7
  69. package/src/BaseNavigationContainer.tsx +16 -10
  70. package/src/ConsumedParamsContext.tsx +1 -6
  71. package/src/NavigationIndependentTree.tsx +8 -5
  72. package/src/SceneView.tsx +18 -51
  73. package/src/StaticNavigation.tsx +2 -2
  74. package/src/findFocusedRoute.tsx +5 -3
  75. package/src/getActionFromState.tsx +3 -1
  76. package/src/getPathFromState.tsx +130 -59
  77. package/src/getPatternParts.tsx +15 -2
  78. package/src/getStateFromPath.tsx +76 -17
  79. package/src/getStateFromRouteParams.tsx +60 -0
  80. package/src/types.tsx +42 -24
  81. package/src/useDescriptors.tsx +1 -1
  82. package/src/useFocusEvents.tsx +7 -5
  83. package/src/useLazyValue.tsx +2 -2
  84. package/src/useNavigationBuilder.tsx +12 -7
  85. package/src/useOnPreventRemove.tsx +4 -3
  86. package/src/useRouteCache.tsx +1 -4
  87. package/src/useSyncState.tsx +3 -2
@@ -12,6 +12,7 @@ import useLatestCallback from 'use-latest-callback';
12
12
 
13
13
  import { checkDuplicateRouteNames } from './checkDuplicateRouteNames';
14
14
  import { checkSerializable } from './checkSerializable';
15
+ import { ConsumedParamsContext } from './ConsumedParamsContext';
15
16
  import { NOT_INITIALIZED_ERROR } from './createNavigationContainerRef';
16
17
  import { DeprecatedNavigationInChildContext } from './DeprecatedNavigationInChildContext';
17
18
  import { EnsureSingleNavigator } from './EnsureSingleNavigator';
@@ -30,6 +31,7 @@ import { UnhandledActionContext } from './UnhandledActionContext';
30
31
  import { useChildListeners } from './useChildListeners';
31
32
  import { useEventEmitter } from './useEventEmitter';
32
33
  import { useKeyedChildListeners } from './useKeyedChildListeners';
34
+ import { useLazyValue } from './useLazyValue';
33
35
  import { useNavigationIndependentTree } from './useNavigationIndependentTree';
34
36
  import { useOptionsGetters } from './useOptionsGetters';
35
37
  import { useSyncState } from './useSyncState';
@@ -108,6 +110,8 @@ export const BaseNavigationContainer = React.forwardRef(
108
110
  getPartialState(initialState == null ? undefined : initialState)
109
111
  );
110
112
 
113
+ const consumedParams = useLazyValue(() => new WeakMap<object, true>());
114
+
111
115
  const isFirstMountRef = React.useRef<boolean>(true);
112
116
 
113
117
  const navigatorKeyRef = React.useRef<string | undefined>(undefined);
@@ -458,17 +462,19 @@ export const BaseNavigationContainer = React.forwardRef(
458
462
  <NavigationContainerRefContext.Provider value={navigation}>
459
463
  <NavigationBuilderContext.Provider value={builderContext}>
460
464
  <NavigationStateContext.Provider value={context}>
461
- <UnhandledActionContext.Provider
462
- value={onUnhandledAction ?? defaultOnUnhandledAction}
463
- >
464
- <DeprecatedNavigationInChildContext.Provider
465
- value={navigationInChildEnabled}
465
+ <ConsumedParamsContext.Provider value={consumedParams}>
466
+ <UnhandledActionContext.Provider
467
+ value={onUnhandledAction ?? defaultOnUnhandledAction}
466
468
  >
467
- <EnsureSingleNavigator>
468
- <ThemeProvider value={theme}>{children}</ThemeProvider>
469
- </EnsureSingleNavigator>
470
- </DeprecatedNavigationInChildContext.Provider>
471
- </UnhandledActionContext.Provider>
469
+ <DeprecatedNavigationInChildContext.Provider
470
+ value={navigationInChildEnabled}
471
+ >
472
+ <EnsureSingleNavigator>
473
+ <ThemeProvider value={theme}>{children}</ThemeProvider>
474
+ </EnsureSingleNavigator>
475
+ </DeprecatedNavigationInChildContext.Provider>
476
+ </UnhandledActionContext.Provider>
477
+ </ConsumedParamsContext.Provider>
472
478
  </NavigationStateContext.Provider>
473
479
  </NavigationBuilderContext.Provider>
474
480
  </NavigationContainerRefContext.Provider>
@@ -1,10 +1,5 @@
1
1
  import * as React from 'react';
2
2
 
3
- type ConsumedParamsContextValue = {
4
- isConsumed: (params: object) => boolean;
5
- setConsumed: (params: object) => void;
6
- };
7
-
8
3
  export const ConsumedParamsContext = React.createContext<
9
- ConsumedParamsContextValue | undefined
4
+ WeakMap<object, true> | undefined
10
5
  >(undefined);
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  import { NavigationContext } from './NavigationContext';
4
+ import { NavigationFocusedRouteStateContext } from './NavigationFocusedRouteStateContext';
4
5
  import { NavigationIndependentTreeContext } from './NavigationIndependentTreeContext';
5
6
  import { NavigationRouteContext } from './NavigationProvider';
6
7
  import { IsFocusedContext } from './useIsFocused';
@@ -17,11 +18,13 @@ export function NavigationIndependentTree({
17
18
  // We need to clear any existing contexts for nested independent container to work correctly
18
19
  <NavigationRouteContext.Provider value={undefined}>
19
20
  <NavigationContext.Provider value={undefined}>
20
- <IsFocusedContext.Provider value={undefined}>
21
- <NavigationIndependentTreeContext.Provider value>
22
- {children}
23
- </NavigationIndependentTreeContext.Provider>
24
- </IsFocusedContext.Provider>
21
+ <NavigationFocusedRouteStateContext.Provider value={undefined}>
22
+ <IsFocusedContext.Provider value={undefined}>
23
+ <NavigationIndependentTreeContext.Provider value>
24
+ {children}
25
+ </NavigationIndependentTreeContext.Provider>
26
+ </IsFocusedContext.Provider>
27
+ </NavigationFocusedRouteStateContext.Provider>
25
28
  </NavigationContext.Provider>
26
29
  </NavigationRouteContext.Provider>
27
30
  );
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 {
@@ -114,36 +113,6 @@ export function SceneView<
114
113
  const parentFocusedRouteState = React.useContext(
115
114
  NavigationFocusedRouteStateContext
116
115
  );
117
- const [consumedParams, setConsumedParams] = React.useState<WeakMap<
118
- object,
119
- true
120
- > | null>(null);
121
-
122
- const consumedParamsContext = React.useMemo(
123
- () => ({
124
- isConsumed: (params: object) => {
125
- if (consumedParams) {
126
- return consumedParams.has(params);
127
- }
128
-
129
- return false;
130
- },
131
- setConsumed: (params: object) => {
132
- setConsumedParams((prev) => {
133
- if (prev && prev.has(params)) {
134
- return prev;
135
- }
136
-
137
- const map = new WeakMap();
138
-
139
- map.set(params, true);
140
-
141
- return map;
142
- });
143
- },
144
- }),
145
- [consumedParams]
146
- );
147
116
 
148
117
  const focusedRouteState = React.useMemo(() => {
149
118
  const state: FocusedRouteState = {
@@ -212,25 +181,23 @@ export function SceneView<
212
181
  : screen.component;
213
182
 
214
183
  return (
215
- <ConsumedParamsContext.Provider value={consumedParamsContext}>
216
- <NavigationStateContext.Provider value={context}>
217
- <NavigationFocusedRouteStateContext.Provider value={focusedRouteState}>
218
- <EnsureSingleNavigator>
219
- <StaticContainer
220
- name={screen.name}
221
- render={ScreenComponent || screen.children}
222
- navigation={navigation}
223
- route={route}
224
- >
225
- {ScreenComponent !== undefined ? (
226
- <ScreenComponent navigation={navigation} route={route} />
227
- ) : screen.children !== undefined ? (
228
- screen.children({ navigation, route })
229
- ) : null}
230
- </StaticContainer>
231
- </EnsureSingleNavigator>
232
- </NavigationFocusedRouteStateContext.Provider>
233
- </NavigationStateContext.Provider>
234
- </ConsumedParamsContext.Provider>
184
+ <NavigationStateContext.Provider value={context}>
185
+ <NavigationFocusedRouteStateContext.Provider value={focusedRouteState}>
186
+ <EnsureSingleNavigator>
187
+ <StaticContainer
188
+ name={screen.name}
189
+ render={ScreenComponent || screen.children}
190
+ navigation={navigation}
191
+ route={route}
192
+ >
193
+ {ScreenComponent !== undefined ? (
194
+ <ScreenComponent navigation={navigation} route={route} />
195
+ ) : screen.children !== undefined ? (
196
+ screen.children({ navigation, route })
197
+ ) : null}
198
+ </StaticContainer>
199
+ </EnsureSingleNavigator>
200
+ </NavigationFocusedRouteStateContext.Provider>
201
+ </NavigationStateContext.Provider>
235
202
  );
236
203
  }
@@ -787,8 +787,8 @@ export function createPathConfigForStaticNavigation(
787
787
 
788
788
  screenConfig.path = normalizePath(
789
789
  key
790
- .replace(/([A-Z]+)/g, '-$1')
791
- .replace(/^-/, '')
790
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
791
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
792
792
  .toLowerCase()
793
793
  );
794
794
  }
@@ -12,11 +12,13 @@ type Result =
12
12
  export function findFocusedRoute(state: InitialState): Result {
13
13
  let current: InitialState | undefined = state;
14
14
 
15
- while (current?.routes[current.index ?? 0].state != null) {
16
- current = current.routes[current.index ?? 0].state;
15
+ while (
16
+ current?.routes[current.index ?? current.routes.length - 1]?.state != null
17
+ ) {
18
+ current = current.routes[current.index ?? current.routes.length - 1]?.state;
17
19
  }
18
20
 
19
- const route = current?.routes[current?.index ?? 0];
21
+ const route = current?.routes[current?.index ?? current.routes.length - 1];
20
22
 
21
23
  return route;
22
24
  }
@@ -180,7 +180,9 @@ export function getActionFromState(
180
180
  };
181
181
  }
182
182
 
183
- const createNormalizedConfigItem = (config: PathConfig<object> | string) =>
183
+ const createNormalizedConfigItem = (
184
+ config: PathConfig<object> | string
185
+ ): ConfigItem =>
184
186
  typeof config === 'object' && config != null
185
187
  ? {
186
188
  initialRouteName: config.initialRouteName,
@@ -1,11 +1,8 @@
1
- import type {
2
- NavigationState,
3
- PartialState,
4
- Route,
5
- } from '@react-navigation/routers';
1
+ import type { NavigationState, PartialState } from '@react-navigation/routers';
6
2
  import * as queryString from 'query-string';
7
3
 
8
4
  import { getPatternParts, type PatternPart } from './getPatternParts';
5
+ import { getStateFromRouteParams } from './getStateFromRouteParams';
9
6
  import type { PathConfig, PathConfigMap } from './types';
10
7
  import { validatePathConfig } from './validatePathConfig';
11
8
 
@@ -19,20 +16,40 @@ type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
19
16
 
20
17
  type StringifyConfig = Record<string, (value: unknown) => string>;
21
18
 
19
+ type SerializedParamValue = string | string[] | null;
20
+
22
21
  type ConfigItem = {
23
22
  parts?: PatternPart[];
23
+ ownParts: PatternPart[];
24
24
  stringify?: StringifyConfig;
25
25
  screens?: Record<string, ConfigItem>;
26
26
  };
27
27
 
28
- const getActiveRoute = (state: State): { name: string; params?: object } => {
28
+ const serializeParamValue = (value: unknown): SerializedParamValue =>
29
+ value === null
30
+ ? null
31
+ : Array.isArray(value)
32
+ ? value.map(String)
33
+ : String(value);
34
+
35
+ const getActiveRoute = (
36
+ state: State,
37
+ configs: Record<string, ConfigItem> | undefined,
38
+ getRouteState: (
39
+ route: State['routes'][number],
40
+ config: ConfigItem | undefined
41
+ ) => State | undefined
42
+ ): { name: string; params?: object } => {
29
43
  const route =
30
44
  typeof state.index === 'number'
31
45
  ? state.routes[state.index]
32
46
  : state.routes[state.routes.length - 1];
33
47
 
34
- if (route.state) {
35
- return getActiveRoute(route.state);
48
+ const config = configs?.[route.name];
49
+ const routeState = getRouteState(route, config);
50
+
51
+ if (routeState) {
52
+ return getActiveRoute(routeState, config?.screens, getRouteState);
36
53
  }
37
54
 
38
55
  return route;
@@ -101,24 +118,55 @@ export function getPathFromState<ParamList extends {}>(
101
118
  }
102
119
 
103
120
  const configs = getNormalizedConfigs(options);
121
+ const cachedRouteStates = new Map<object, State | undefined>();
122
+
123
+ const getRouteState = (
124
+ route: State['routes'][number],
125
+ config: ConfigItem | undefined
126
+ ) => {
127
+ if (route.state) {
128
+ return route.state;
129
+ }
130
+
131
+ const hasScreenParams =
132
+ route.params &&
133
+ 'screen' in route.params &&
134
+ typeof route.params.screen === 'string' &&
135
+ config?.screens?.[route.params.screen];
136
+
137
+ const hasStateParams =
138
+ route.params && 'state' in route.params && config?.screens;
139
+
140
+ if (
141
+ !route.params ||
142
+ !config?.screens ||
143
+ (!hasScreenParams && !hasStateParams)
144
+ ) {
145
+ return undefined;
146
+ }
147
+
148
+ if (!cachedRouteStates.has(route)) {
149
+ cachedRouteStates.set(route, getStateFromRouteParams(route.params));
150
+ }
151
+
152
+ return cachedRouteStates.get(route);
153
+ };
154
+
155
+ const focusedRoute = getActiveRoute(state, configs, getRouteState);
104
156
 
105
157
  let path = '/';
106
158
  let current: State | undefined = state;
107
159
 
108
- const allParams: Record<string, string> = {};
109
-
110
160
  while (current) {
111
161
  let index = typeof current.index === 'number' ? current.index : 0;
112
- let route = current.routes[index] as Route<string> & {
113
- state?: State;
114
- };
162
+ let route = current.routes[index];
115
163
 
116
164
  let parts: PatternPart[] | undefined;
117
165
 
118
- let focusedParams: Record<string, string> | undefined;
119
- let currentOptions = configs;
166
+ const partValues = new Map<PatternPart, SerializedParamValue | undefined>();
120
167
 
121
- const focusedRoute = getActiveRoute(state);
168
+ let focusedParams: Record<string, SerializedParamValue> | undefined;
169
+ let currentOptions = configs;
122
170
 
123
171
  // Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
124
172
  const nestedRouteNames = [];
@@ -126,19 +174,21 @@ export function getPathFromState<ParamList extends {}>(
126
174
  let hasNext = true;
127
175
 
128
176
  while (route.name in currentOptions && hasNext) {
129
- parts = currentOptions[route.name].parts;
177
+ const config = currentOptions[route.name];
178
+ parts = config.parts;
179
+ const ownParts = config.ownParts;
130
180
 
131
181
  nestedRouteNames.push(route.name);
132
182
 
133
183
  if (route.params) {
134
- const options = currentOptions[route.name];
184
+ const options = config;
135
185
 
136
186
  const currentParams = Object.fromEntries(
137
187
  Object.entries(route.params)
138
- .map(([key, value]): [string, string] | null => {
188
+ .map(([key, value]): [string, SerializedParamValue] | null => {
139
189
  if (value === undefined) {
140
190
  if (options) {
141
- const optional = options.parts?.find(
191
+ const optional = ownParts.find(
142
192
  (part) => part.param === key
143
193
  )?.optional;
144
194
 
@@ -150,51 +200,71 @@ export function getPathFromState<ParamList extends {}>(
150
200
  }
151
201
  }
152
202
 
153
- const stringify = options?.stringify?.[key] ?? String;
203
+ const stringify = options?.stringify?.[key];
154
204
 
155
- return [key, stringify(value)];
205
+ return [
206
+ key,
207
+ stringify ? stringify(value) : serializeParamValue(value),
208
+ ];
156
209
  })
157
210
  .filter((entry) => entry != null)
158
211
  );
159
212
 
160
- if (parts?.length) {
161
- Object.assign(allParams, currentParams);
213
+ const claimedParams = new Set<string>();
214
+
215
+ for (const part of ownParts) {
216
+ if (part.param && part.param in currentParams) {
217
+ const value = currentParams[part.param];
218
+
219
+ if (value !== undefined) {
220
+ partValues.set(part, value);
221
+ claimedParams.add(part.param);
222
+ }
223
+ }
162
224
  }
163
225
 
164
226
  if (focusedRoute === route) {
165
227
  // If this is the focused route, keep the params for later use
166
228
  // We save it here since it's been stringified already
167
- focusedParams = { ...currentParams };
168
-
169
- parts
170
- // eslint-disable-next-line no-loop-func
171
- ?.forEach(({ param }) => {
172
- if (param) {
173
- // Remove the params present in the pattern since we'll only use the rest for query string
174
- if (focusedParams) {
175
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
176
- delete focusedParams[param];
177
- }
178
- }
179
- });
229
+ focusedParams = {};
230
+
231
+ for (const key in currentParams) {
232
+ const value = currentParams[key];
233
+
234
+ if (
235
+ !claimedParams.has(key) &&
236
+ value !== undefined &&
237
+ value !== 'undefined'
238
+ ) {
239
+ focusedParams[key] = value;
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ for (const part of ownParts) {
246
+ if (part.param && !partValues.has(part)) {
247
+ partValues.set(part, undefined);
180
248
  }
181
249
  }
182
250
 
251
+ const routeState = getRouteState(route, config);
252
+
183
253
  // If there is no `screens` property or no nested state, we return pattern
184
- if (!currentOptions[route.name].screens || route.state === undefined) {
254
+ if (!config.screens || routeState === undefined) {
185
255
  hasNext = false;
186
256
  } else {
187
257
  index =
188
- typeof route.state.index === 'number'
189
- ? route.state.index
190
- : route.state.routes.length - 1;
258
+ typeof routeState.index === 'number'
259
+ ? routeState.index
260
+ : routeState.routes.length - 1;
191
261
 
192
- const nextRoute = route.state.routes[index];
193
- const nestedConfig = currentOptions[route.name].screens;
262
+ const nextRoute = routeState.routes[index];
263
+ const nestedConfig = config.screens;
194
264
 
195
265
  // if there is config for next route name, we go deeper
196
266
  if (nestedConfig && nextRoute.name in nestedConfig) {
197
- route = nextRoute as Route<string> & { state?: State };
267
+ route = nextRoute;
198
268
  currentOptions = nestedConfig;
199
269
  } else {
200
270
  // If not, there is no sense in going deeper in config
@@ -203,9 +273,12 @@ export function getPathFromState<ParamList extends {}>(
203
273
  }
204
274
  }
205
275
 
276
+ const routeState = getRouteState(route, currentOptions[route.name]);
277
+
206
278
  if (currentOptions[route.name] !== undefined) {
207
279
  path += parts
208
- ?.map(({ segment, param, optional }) => {
280
+ ?.map((part) => {
281
+ const { segment, param, optional } = part;
209
282
  // We don't know what to show for wildcard patterns
210
283
  // Showing the route name seems ok, though whatever we show here will be incorrect
211
284
  // Since the page doesn't actually exist
@@ -215,7 +288,7 @@ export function getPathFromState<ParamList extends {}>(
215
288
 
216
289
  // If the path has a pattern for a param, put the param in the path
217
290
  if (param) {
218
- const value = allParams[param];
291
+ const value = partValues.get(part);
219
292
 
220
293
  if (value === undefined && optional) {
221
294
  // Optional params without value assigned in route.params should be ignored
@@ -244,12 +317,12 @@ export function getPathFromState<ParamList extends {}>(
244
317
  focusedParams = Object.fromEntries(
245
318
  Object.entries(focusedRoute.params).map(([key, value]) => [
246
319
  key,
247
- String(value),
320
+ serializeParamValue(value),
248
321
  ])
249
322
  );
250
323
  }
251
324
 
252
- if (route.state) {
325
+ if (routeState) {
253
326
  path += '/';
254
327
  } else if (focusedParams) {
255
328
  for (const param in focusedParams) {
@@ -266,7 +339,7 @@ export function getPathFromState<ParamList extends {}>(
266
339
  }
267
340
  }
268
341
 
269
- current = route.state;
342
+ current = routeState;
270
343
  }
271
344
 
272
345
  // Include the root path if specified
@@ -292,14 +365,13 @@ const createConfigItem = (
292
365
  parentParts?: PatternPart[]
293
366
  ): ConfigItem => {
294
367
  if (typeof config === 'string') {
295
- // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
296
- const parts = getPatternParts(config);
368
+ const ownParts = getPatternParts(config);
297
369
 
298
370
  if (parentParts) {
299
- return { parts: [...parentParts, ...parts] };
371
+ return { parts: [...parentParts, ...ownParts], ownParts };
300
372
  }
301
373
 
302
- return { parts };
374
+ return { parts: ownParts, ownParts };
303
375
  }
304
376
 
305
377
  if (config.exact && config.path === undefined) {
@@ -310,14 +382,12 @@ const createConfigItem = (
310
382
 
311
383
  // If an object is specified as the value (e.g. Foo: { ... }),
312
384
  // It can have `path` property and `screens` prop which has nested configs
385
+ const ownParts = config.path ? getPatternParts(config.path) : [];
313
386
  const parts =
314
387
  config.exact !== true
315
- ? [
316
- ...(parentParts || []),
317
- ...(config.path ? getPatternParts(config.path) : []),
318
- ]
319
- : config.path
320
- ? getPatternParts(config.path)
388
+ ? [...(parentParts || []), ...ownParts]
389
+ : ownParts.length
390
+ ? ownParts
321
391
  : undefined;
322
392
 
323
393
  const screens = config.screens
@@ -326,6 +396,7 @@ const createConfigItem = (
326
396
 
327
397
  return {
328
398
  parts,
399
+ ownParts,
329
400
  stringify: config.stringify,
330
401
  screens,
331
402
  };
@@ -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) {