@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.
Files changed (72) hide show
  1. package/lib/module/PreventRemoveContext.js.map +1 -1
  2. package/lib/module/PreventRemoveProvider.js +34 -24
  3. package/lib/module/PreventRemoveProvider.js.map +1 -1
  4. package/lib/module/createNavigationContainerRef.js +6 -8
  5. package/lib/module/createNavigationContainerRef.js.map +1 -1
  6. package/lib/module/getStateFromPath.js +82 -58
  7. package/lib/module/getStateFromPath.js.map +1 -1
  8. package/lib/module/useEventEmitter.js +1 -1
  9. package/lib/module/useEventEmitter.js.map +1 -1
  10. package/lib/module/useKeyedChildListeners.js +4 -2
  11. package/lib/module/useKeyedChildListeners.js.map +1 -1
  12. package/lib/module/useNavigationBuilder.js +3 -15
  13. package/lib/module/useNavigationBuilder.js.map +1 -1
  14. package/lib/module/useNavigationCache.js +5 -2
  15. package/lib/module/useNavigationCache.js.map +1 -1
  16. package/lib/module/useNavigationHelpers.js +27 -9
  17. package/lib/module/useNavigationHelpers.js.map +1 -1
  18. package/lib/module/useNavigationState.js +1 -1
  19. package/lib/module/useNavigationState.js.map +1 -1
  20. package/lib/module/useOnAction.js +1 -1
  21. package/lib/module/useOnAction.js.map +1 -1
  22. package/lib/module/useOptionsGetters.js +3 -1
  23. package/lib/module/useOptionsGetters.js.map +1 -1
  24. package/lib/module/usePreventRemove.js +9 -2
  25. package/lib/module/usePreventRemove.js.map +1 -1
  26. package/lib/module/useRouteCache.js +14 -7
  27. package/lib/module/useRouteCache.js.map +1 -1
  28. package/lib/module/useScheduleUpdate.js +8 -8
  29. package/lib/module/useScheduleUpdate.js.map +1 -1
  30. package/lib/module/useSyncState.js +4 -1
  31. package/lib/module/useSyncState.js.map +1 -1
  32. package/lib/typescript/src/PreventRemoveContext.d.ts +1 -0
  33. package/lib/typescript/src/PreventRemoveContext.d.ts.map +1 -1
  34. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  35. package/lib/typescript/src/StaticNavigation.d.ts +2 -2
  36. package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
  37. package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
  38. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  39. package/lib/typescript/src/types.d.ts +3 -3
  40. package/lib/typescript/src/types.d.ts.map +1 -1
  41. package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
  42. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  43. package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
  44. package/lib/typescript/src/useNavigationHelpers.d.ts +2 -3
  45. package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
  46. package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
  47. package/lib/typescript/src/usePreventRemove.d.ts.map +1 -1
  48. package/lib/typescript/src/usePreventRemoveContext.d.ts +1 -0
  49. package/lib/typescript/src/usePreventRemoveContext.d.ts.map +1 -1
  50. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  51. package/lib/typescript/src/useScheduleUpdate.d.ts +5 -4
  52. package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
  53. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  54. package/package.json +2 -2
  55. package/src/PreventRemoveContext.tsx +1 -0
  56. package/src/PreventRemoveProvider.tsx +48 -43
  57. package/src/StaticNavigation.tsx +2 -2
  58. package/src/createNavigationContainerRef.tsx +12 -9
  59. package/src/getStateFromPath.tsx +112 -74
  60. package/src/types.tsx +18 -16
  61. package/src/useEventEmitter.tsx +1 -1
  62. package/src/useKeyedChildListeners.tsx +4 -2
  63. package/src/useNavigationBuilder.tsx +2 -16
  64. package/src/useNavigationCache.tsx +6 -2
  65. package/src/useNavigationHelpers.tsx +26 -9
  66. package/src/useNavigationState.tsx +1 -1
  67. package/src/useOnAction.tsx +1 -1
  68. package/src/useOptionsGetters.tsx +4 -1
  69. package/src/usePreventRemove.tsx +11 -2
  70. package/src/useRouteCache.tsx +19 -11
  71. package/src/useScheduleUpdate.tsx +8 -7
  72. package/src/useSyncState.tsx +5 -1
@@ -26,10 +26,11 @@ type RouteConfig = {
26
26
  screen: string;
27
27
  regex?: RegExp;
28
28
  segments: string[];
29
- params: { screen: string; name?: string; index: number }[];
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
- const decodedSegments: string[] = [];
126
+ if (screens === undefined) {
127
+ // When no config is specified, use the path segments as route names
128
+ const routes: ParsedRoute[] = [];
126
129
 
127
- for (const segment of remaining.split('/')) {
128
- if (!segment) {
129
- continue;
130
- }
130
+ for (const segment of remaining.split('/')) {
131
+ if (!segment) {
132
+ continue;
133
+ }
131
134
 
132
- try {
133
- decodedSegments.push(decodeURIComponent(segment));
134
- } catch {
135
- return undefined;
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
- decodedSegments,
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
- function checkForDuplicatedConfigs(configs: RouteConfig[]) {
334
- // Check for duplicate patterns in the config
335
- configs.reduce<Record<string, RouteConfig>>((acc, config) => {
336
- const pattern = config.segments.join('/');
337
-
338
- if (acc[pattern]) {
339
- const a = acc[pattern].routeNames;
340
- const b = config.routeNames;
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
- return Object.assign(acc, {
361
- [pattern]: config,
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
- decodedSegments: string[],
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
- routes = [];
399
- matchingConfig = config;
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 [key, value] of Object.entries(match.groups)) {
413
- const index = Number(key.replace('param_', ''));
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
- return { routes: undefined, remainingPath };
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
- routes.push({ name: routeName, params });
459
+ matchedRoutes.push({ name: routeName, params });
445
460
  } else {
446
- routes.push({ name: routeName });
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(...getPatternParts(path).map((part) => ({ ...part, screen })));
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 = parts
635
- .map((it, i) =>
636
- it.param
637
- ? {
638
- index: i,
639
- screen: it.screen,
640
- name: it.param,
641
- }
642
- : null
643
- )
644
- .filter((it) => it != null);
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 TypedNavigatorComponent<Bag extends NavigatorTypeBagBase> =
1106
+ type TypedNavigatorStaticComponent<Bag extends NavigatorTypeBagBase> =
1107
1107
  React.ComponentType<
1108
- Omit<
1109
- React.ComponentProps<
1110
- TypedNavigatorInternal<
1111
- Bag['ParamList'],
1112
- Bag['NavigatorID'],
1113
- Bag['State'],
1114
- Bag['ScreenOptions'],
1115
- Bag['EventMap'],
1116
- Bag['NavigationList'],
1117
- Bag['Navigator']
1118
- >['Navigator']
1119
- >,
1120
- 'children'
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: TypedNavigatorComponent<Bag>;
1157
+ Navigator: TypedNavigatorStaticComponent<Bag>;
1156
1158
  }>
1157
1159
  ) => TypedNavigatorStaticDecorated<Bag, Config>;
1158
- getComponent: () => TypedNavigatorComponent<Bag>;
1160
+ getComponent: () => TypedNavigatorStaticComponent<Bag>;
1159
1161
  } & PrivateValueStore<[Bag['ParamList'], Bag['NavigationList'], unknown]>;
1160
1162
 
1161
1163
  export type TypedNavigator<
@@ -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.useEffect(() => {
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
- // @ts-expect-error: according to ref stated above you can use `key` to index type
32
- keyedListeners[type][key] = undefined;
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.useEffect(() => {
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
- cache.current = state.routes.reduce<
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: cache.current,
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
- // By this time, we haven't committed the new state yet
108
- // Without this `useSyncExternalStore` will keep reading the old state
109
- // This may result in `useNavigationState` or `useIsFocused` returning wrong values
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 getState();
132
+ return current;
116
133
  },
117
134
  } as NavigationHelpers<ParamListBase, EventMap> & ActionHelpers;
118
135
 
@@ -54,7 +54,7 @@ export function NavigationStateListenerProvider({
54
54
  };
55
55
  });
56
56
 
57
- React.useLayoutEffect(() => {
57
+ React.useInsertionEffect(() => {
58
58
  stateRef.current = state;
59
59
  listeners.current.forEach((callback) => callback());
60
60
  }, [state]);
@@ -60,7 +60,7 @@ export function useOnAction<State extends NavigationState>({
60
60
  const routerConfigOptionsRef =
61
61
  React.useRef<RouterConfigOptions>(routerConfigOptions);
62
62
 
63
- React.useEffect(() => {
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.useEffect(() => {
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);
@@ -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.useEffect(() => {
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) => {
@@ -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(() => ({ current: new Map() as RouteCache }), []);
22
+ const cache = React.useMemo(
23
+ () => ({ current: new Map<string, RouteProp<ParamListBase>>() }),
24
+ []
25
+ );
25
26
 
26
- cache.current = routes.reduce((acc, route) => {
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
- Object.defineProperty(proxy, CHILD_STATE, {
57
- enumerable: false,
58
- configurable: true,
59
- value: state,
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() as RouteCache);
69
+ }, new Map<string, RouteProp<ParamListBase>>());
70
+
71
+ React.useInsertionEffect(() => {
72
+ cache.current = next;
73
+ });
66
74
 
67
- return Array.from(cache.current.values());
75
+ return Array.from(next.values());
68
76
  }