@react-navigation/core 7.21.3 → 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 (62) 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/useEventEmitter.js +1 -1
  7. package/lib/module/useEventEmitter.js.map +1 -1
  8. package/lib/module/useKeyedChildListeners.js +4 -2
  9. package/lib/module/useKeyedChildListeners.js.map +1 -1
  10. package/lib/module/useNavigationBuilder.js +3 -15
  11. package/lib/module/useNavigationBuilder.js.map +1 -1
  12. package/lib/module/useNavigationCache.js +5 -2
  13. package/lib/module/useNavigationCache.js.map +1 -1
  14. package/lib/module/useNavigationHelpers.js +27 -9
  15. package/lib/module/useNavigationHelpers.js.map +1 -1
  16. package/lib/module/useNavigationState.js +1 -1
  17. package/lib/module/useNavigationState.js.map +1 -1
  18. package/lib/module/useOnAction.js +1 -1
  19. package/lib/module/useOnAction.js.map +1 -1
  20. package/lib/module/useOptionsGetters.js +3 -1
  21. package/lib/module/useOptionsGetters.js.map +1 -1
  22. package/lib/module/usePreventRemove.js +9 -2
  23. package/lib/module/usePreventRemove.js.map +1 -1
  24. package/lib/module/useRouteCache.js +14 -7
  25. package/lib/module/useRouteCache.js.map +1 -1
  26. package/lib/module/useScheduleUpdate.js +8 -8
  27. package/lib/module/useScheduleUpdate.js.map +1 -1
  28. package/lib/module/useSyncState.js +4 -1
  29. package/lib/module/useSyncState.js.map +1 -1
  30. package/lib/typescript/src/PreventRemoveContext.d.ts +1 -0
  31. package/lib/typescript/src/PreventRemoveContext.d.ts.map +1 -1
  32. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  33. package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
  34. package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
  35. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  36. package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
  37. package/lib/typescript/src/useNavigationHelpers.d.ts +2 -3
  38. package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
  39. package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
  40. package/lib/typescript/src/usePreventRemove.d.ts.map +1 -1
  41. package/lib/typescript/src/usePreventRemoveContext.d.ts +1 -0
  42. package/lib/typescript/src/usePreventRemoveContext.d.ts.map +1 -1
  43. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  44. package/lib/typescript/src/useScheduleUpdate.d.ts +5 -4
  45. package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
  46. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  47. package/package.json +2 -2
  48. package/src/PreventRemoveContext.tsx +1 -0
  49. package/src/PreventRemoveProvider.tsx +48 -43
  50. package/src/createNavigationContainerRef.tsx +12 -9
  51. package/src/useEventEmitter.tsx +1 -1
  52. package/src/useKeyedChildListeners.tsx +4 -2
  53. package/src/useNavigationBuilder.tsx +2 -16
  54. package/src/useNavigationCache.tsx +6 -2
  55. package/src/useNavigationHelpers.tsx +26 -9
  56. package/src/useNavigationState.tsx +1 -1
  57. package/src/useOnAction.tsx +1 -1
  58. package/src/useOptionsGetters.tsx +4 -1
  59. package/src/usePreventRemove.tsx +11 -2
  60. package/src/useRouteCache.tsx +19 -11
  61. package/src/useScheduleUpdate.tsx +8 -7
  62. package/src/useSyncState.tsx +5 -1
@@ -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
  }
@@ -5,19 +5,20 @@ import { useClientLayoutEffect } from './useClientLayoutEffect';
5
5
 
6
6
  /**
7
7
  * When screen config changes, we want to update the navigator in the same update phase.
8
- * However, navigation state is in the root component and React won't let us update it from a child.
9
- * This is a workaround for that, the scheduled update is stored in the ref without actually calling setState.
10
- * It lets all subsequent updates access the latest state so it stays correct.
11
- * Then we call setState during after the component updates.
8
+ * However, navigation state is in the root component and React won't let us update it from a child during render.
9
+ * The render could also be interrupted in concurrent mode, so we need to update after commit.
10
+ * So we queue the update in an insertion effect and flush it in a layout effect.
11
+ * Insertion effects run before layout effects, so every navigator in the tree has queued its update before we flush.
12
+ * This lets nested updates be applied from the root down without clobbering each other.
12
13
  */
13
14
  export function useScheduleUpdate(callback: () => void) {
14
15
  const { scheduleUpdate, flushUpdates } = React.useContext(
15
16
  NavigationBuilderContext
16
17
  );
17
18
 
18
- // FIXME: This is potentially unsafe
19
- // However, since we are using sync store, it might be fine
20
- scheduleUpdate(callback);
19
+ React.useInsertionEffect(() => {
20
+ scheduleUpdate(callback);
21
+ });
21
22
 
22
23
  useClientLayoutEffect(flushUpdates);
23
24
  }
@@ -87,7 +87,11 @@ export function useSyncState<T>(getInitialState: () => T) {
87
87
 
88
88
  if (pendingUpdates.length !== 0) {
89
89
  store.batchUpdates(() => {
90
- // Flush all the pending updates
90
+ // Updates are queued child-first as insertion effects run bottom-up.
91
+ // The root navigator replaces the whole state while nested navigators merge into it.
92
+ // So we apply them root-first to avoid the root clobbering nested updates.
93
+ pendingUpdates.reverse();
94
+
91
95
  for (const update of pendingUpdates) {
92
96
  update();
93
97
  }