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

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 (136) hide show
  1. package/lib/module/BaseNavigationContainer.js +5 -2
  2. package/lib/module/BaseNavigationContainer.js.map +1 -1
  3. package/lib/module/DataLoading.js +102 -0
  4. package/lib/module/DataLoading.js.map +1 -0
  5. package/lib/module/NavigationIndependentTree.js +7 -6
  6. package/lib/module/NavigationIndependentTree.js.map +1 -1
  7. package/lib/module/NavigationProvider.js +12 -12
  8. package/lib/module/NavigationProvider.js.map +1 -1
  9. package/lib/module/PreventRemoveContext.js.map +1 -1
  10. package/lib/module/PreventRemoveProvider.js +34 -24
  11. package/lib/module/PreventRemoveProvider.js.map +1 -1
  12. package/lib/module/StaticNavigation.js +14 -6
  13. package/lib/module/StaticNavigation.js.map +1 -1
  14. package/lib/module/StaticTreeContext.js +5 -0
  15. package/lib/module/StaticTreeContext.js.map +1 -0
  16. package/lib/module/checkSerializable.js +7 -9
  17. package/lib/module/checkSerializable.js.map +1 -1
  18. package/lib/module/createNavigationContainerRef.js +6 -8
  19. package/lib/module/createNavigationContainerRef.js.map +1 -1
  20. package/lib/module/createNavigatorFactory.js.map +1 -1
  21. package/lib/module/getActionFromState.js +11 -11
  22. package/lib/module/getActionFromState.js.map +1 -1
  23. package/lib/module/getPathFromState.js +38 -39
  24. package/lib/module/getPathFromState.js.map +1 -1
  25. package/lib/module/getPatternParts.js +2 -2
  26. package/lib/module/getPatternParts.js.map +1 -1
  27. package/lib/module/getStateFromPath.js +72 -45
  28. package/lib/module/getStateFromPath.js.map +1 -1
  29. package/lib/module/index.js +1 -0
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/module/types.js.map +1 -1
  32. package/lib/module/useEventEmitter.js +1 -1
  33. package/lib/module/useEventEmitter.js.map +1 -1
  34. package/lib/module/useKeyedChildListeners.js +4 -2
  35. package/lib/module/useKeyedChildListeners.js.map +1 -1
  36. package/lib/module/useNavigation.js +2 -15
  37. package/lib/module/useNavigation.js.map +1 -1
  38. package/lib/module/useNavigationBuilder.js +4 -14
  39. package/lib/module/useNavigationBuilder.js.map +1 -1
  40. package/lib/module/useNavigationCache.js +19 -14
  41. package/lib/module/useNavigationCache.js.map +1 -1
  42. package/lib/module/useNavigationHelpers.js +27 -9
  43. package/lib/module/useNavigationHelpers.js.map +1 -1
  44. package/lib/module/useNavigationState.js +69 -36
  45. package/lib/module/useNavigationState.js.map +1 -1
  46. package/lib/module/useOnAction.js +21 -2
  47. package/lib/module/useOnAction.js.map +1 -1
  48. package/lib/module/useOptionsGetters.js +3 -1
  49. package/lib/module/useOptionsGetters.js.map +1 -1
  50. package/lib/module/usePreventRemove.js +9 -2
  51. package/lib/module/usePreventRemove.js.map +1 -1
  52. package/lib/module/useRouteCache.js +14 -7
  53. package/lib/module/useRouteCache.js.map +1 -1
  54. package/lib/module/useScheduleUpdate.js +8 -8
  55. package/lib/module/useScheduleUpdate.js.map +1 -1
  56. package/lib/module/useSyncState.js +4 -1
  57. package/lib/module/useSyncState.js.map +1 -1
  58. package/lib/module/validatePathConfig.js.map +1 -1
  59. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  60. package/lib/typescript/src/DataLoading.d.ts +20 -0
  61. package/lib/typescript/src/DataLoading.d.ts.map +1 -0
  62. package/lib/typescript/src/NavigationProvider.d.ts +0 -1
  63. package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
  64. package/lib/typescript/src/PreventRemoveContext.d.ts +1 -0
  65. package/lib/typescript/src/PreventRemoveContext.d.ts.map +1 -1
  66. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  67. package/lib/typescript/src/StaticNavigation.d.ts +65 -39
  68. package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
  69. package/lib/typescript/src/StaticTreeContext.d.ts +4 -0
  70. package/lib/typescript/src/StaticTreeContext.d.ts.map +1 -0
  71. package/lib/typescript/src/checkSerializable.d.ts +4 -2
  72. package/lib/typescript/src/checkSerializable.d.ts.map +1 -1
  73. package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
  74. package/lib/typescript/src/createNavigatorFactory.d.ts +9 -5
  75. package/lib/typescript/src/createNavigatorFactory.d.ts.map +1 -1
  76. package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
  77. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  78. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  79. package/lib/typescript/src/index.d.ts +2 -1
  80. package/lib/typescript/src/index.d.ts.map +1 -1
  81. package/lib/typescript/src/types.d.ts +19 -13
  82. package/lib/typescript/src/types.d.ts.map +1 -1
  83. package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
  84. package/lib/typescript/src/useNavigation.d.ts.map +1 -1
  85. package/lib/typescript/src/useNavigationBuilder.d.ts +1 -1
  86. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  87. package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
  88. package/lib/typescript/src/useNavigationHelpers.d.ts +3 -4
  89. package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
  90. package/lib/typescript/src/useNavigationState.d.ts +11 -1
  91. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  92. package/lib/typescript/src/useOnAction.d.ts.map +1 -1
  93. package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
  94. package/lib/typescript/src/usePreventRemove.d.ts.map +1 -1
  95. package/lib/typescript/src/usePreventRemoveContext.d.ts +1 -0
  96. package/lib/typescript/src/usePreventRemoveContext.d.ts.map +1 -1
  97. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  98. package/lib/typescript/src/useScheduleUpdate.d.ts +5 -4
  99. package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
  100. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  101. package/lib/typescript/src/utilities.d.ts +17 -0
  102. package/lib/typescript/src/utilities.d.ts.map +1 -1
  103. package/lib/typescript/src/validatePathConfig.d.ts.map +1 -1
  104. package/package.json +3 -5
  105. package/src/BaseNavigationContainer.tsx +9 -2
  106. package/src/DataLoading.tsx +139 -0
  107. package/src/NavigationIndependentTree.tsx +11 -11
  108. package/src/NavigationProvider.tsx +13 -16
  109. package/src/PreventRemoveContext.tsx +1 -0
  110. package/src/PreventRemoveProvider.tsx +48 -43
  111. package/src/StaticNavigation.tsx +157 -93
  112. package/src/StaticTreeContext.tsx +7 -0
  113. package/src/checkSerializable.tsx +20 -14
  114. package/src/createNavigationContainerRef.tsx +12 -9
  115. package/src/createNavigatorFactory.tsx +16 -28
  116. package/src/getActionFromState.tsx +18 -16
  117. package/src/getPathFromState.tsx +69 -65
  118. package/src/getPatternParts.tsx +2 -2
  119. package/src/getStateFromPath.tsx +107 -66
  120. package/src/index.tsx +2 -0
  121. package/src/types.tsx +62 -50
  122. package/src/useEventEmitter.tsx +1 -1
  123. package/src/useKeyedChildListeners.tsx +4 -2
  124. package/src/useNavigation.tsx +2 -21
  125. package/src/useNavigationBuilder.tsx +6 -16
  126. package/src/useNavigationCache.tsx +26 -19
  127. package/src/useNavigationHelpers.tsx +26 -9
  128. package/src/useNavigationState.tsx +90 -60
  129. package/src/useOnAction.tsx +49 -1
  130. package/src/useOptionsGetters.tsx +4 -1
  131. package/src/usePreventRemove.tsx +11 -2
  132. package/src/useRouteCache.tsx +19 -11
  133. package/src/useScheduleUpdate.tsx +8 -7
  134. package/src/useSyncState.tsx +5 -1
  135. package/src/utilities.tsx +28 -0
  136. package/src/validatePathConfig.tsx +1 -0
@@ -19,9 +19,9 @@ type Options<State extends NavigationState, Action extends NavigationAction> = {
19
19
  onAction: (action: NavigationAction) => boolean;
20
20
  onUnhandledAction: (action: NavigationAction) => void;
21
21
  getState: () => State;
22
+ state: State;
22
23
  emitter: NavigationEventEmitter<any>;
23
24
  router: Router<State, Action>;
24
- stateRef: React.RefObject<State | null>;
25
25
  };
26
26
 
27
27
  /**
@@ -37,12 +37,29 @@ export function useNavigationHelpers<
37
37
  onAction,
38
38
  onUnhandledAction,
39
39
  getState,
40
+ state,
40
41
  emitter,
41
42
  router,
42
- stateRef,
43
43
  }: Options<State, Action>) {
44
44
  const parentNavigationHelpers = React.use(NavigationContext);
45
45
 
46
+ // In some cases (e.g. route names change), internal state might have changed
47
+ // But it hasn't been committed yet, so hasn't propagated to any listeners
48
+ // During this time, we need to return the internal state in `getState`
49
+ // Otherwise it can return an inconsistent state during render in children
50
+ // This may affect hooks like `useNavigationState` that need to read the state during render
51
+ // To avoid this, we use a ref for render phase, and immediately clear it on commit
52
+ // The ref won't be cleared if the render is discarded, e.g. when interrupted
53
+ // So we also track the stored state the render was based on
54
+ // Then ignore the ref once the stored state changes due to another dispatch
55
+ const stateRef = React.useRef<{ state: State; base: State } | null>(null);
56
+
57
+ stateRef.current = { state, base: getState() };
58
+
59
+ React.useInsertionEffect(() => {
60
+ stateRef.current = null;
61
+ });
62
+
46
63
  return React.useMemo(() => {
47
64
  const dispatch = (op: Action | ((state: State) => Action)) => {
48
65
  const action = typeof op === 'function' ? op(getState()) : op;
@@ -87,16 +104,16 @@ export function useNavigationHelpers<
87
104
  );
88
105
  },
89
106
  getState: (): State => {
107
+ const current = getState();
108
+ const pending = stateRef.current;
109
+
90
110
  // FIXME: Workaround for when the state is read during render
91
- // By this time, we haven't committed the new state yet
92
- // Without this `useSyncExternalStore` will keep reading the old state
93
- // This may result in `useNavigationState` or `useIsFocused` returning wrong values
94
- // Apart from `useSyncExternalStore`, `getState` should never be called during render
95
- if (stateRef.current != null) {
96
- return stateRef.current;
111
+ // Apart from subscriptions, `getState` should never be called during render
112
+ if (pending != null && pending.base === current) {
113
+ return pending.state;
97
114
  }
98
115
 
99
- return getState();
116
+ return current;
100
117
  },
101
118
  } as NavigationHelpers<ParamListBase, EventMap> & ActionHelpers;
102
119
 
@@ -1,22 +1,14 @@
1
1
  import type { NavigationState, ParamListBase } from '@react-navigation/routers';
2
2
  import * as React from 'react';
3
3
  import useLatestCallback from 'use-latest-callback';
4
- import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector';
5
4
 
6
- import type {
7
- NavigationListForNested,
8
- NavigationProp,
9
- RootNavigator,
10
- } from './types';
11
- import { useNavigation } from './useNavigation';
5
+ import type { NavigationListForNested, RootNavigator } from './types';
12
6
 
13
7
  type NavigationStateListener = {
14
8
  getState: () => NavigationState<ParamListBase>;
15
9
  subscribe: (callback: () => void) => () => void;
16
10
  };
17
11
 
18
- type UseNavigationForName = (name: string) => NavigationProp<ParamListBase>;
19
-
20
12
  type NavigationStateForNested<
21
13
  Navigator,
22
14
  RouteName extends keyof NavigationListForNested<Navigator>,
@@ -47,81 +39,83 @@ export function useNavigationState<T>(
47
39
  selector: (state: NavigationState<ParamListBase>) => T
48
40
  ): T;
49
41
  export function useNavigationState(...args: unknown[]): unknown {
50
- let navigation: NavigationProp<ParamListBase> | undefined,
51
- stateListener: NavigationStateListener | undefined,
52
- selector;
42
+ const selector = typeof args[0] === 'string' ? args[1] : args[0];
53
43
 
54
- if (typeof args[0] === 'string') {
55
- // `useNavigation` uses `use` internally, so it's fine to call it conditionally
56
- // Cast to a non-generic signature to skip overload resolution - otherwise TS
57
- // eagerly expands `NavigationListForNested<RootNavigator>` at this call site.
58
- navigation = (useNavigation as UseNavigationForName)(args[0]);
59
- selector = args[1];
60
- } else {
61
- selector = args[0];
44
+ if (typeof selector !== 'function') {
45
+ throw new Error(
46
+ `A selector function must be provided (got ${typeof selector}).`
47
+ );
62
48
  }
63
49
 
64
- if (navigation == null) {
65
- stateListener = React.use(NavigationStateListenerContext);
50
+ // @ts-expect-error: the public overloads guarantee the selector's type, but
51
+ // the `unknown[]` implementation signature can't express it
52
+ const select: (state: NavigationState<ParamListBase>) => unknown = selector;
53
+
54
+ let listener: NavigationStateListener | undefined;
66
55
 
67
- if (stateListener == null) {
56
+ if (typeof args[0] === 'string') {
57
+ const name = args[0];
58
+ const listeners = React.use(NamedNavigationStateListenerListContext);
59
+
60
+ listener = listeners?.[name];
61
+
62
+ if (listener == null) {
63
+ throw new Error(
64
+ `Couldn't find a navigator for the route '${name}' in any of the parent screens. Is your component inside the correct screen?`
65
+ );
66
+ }
67
+ } else {
68
+ listener = React.use(NavigationStateListenerContext);
69
+
70
+ if (listener == null) {
68
71
  throw new Error(
69
72
  "Couldn't get the navigation state. Is your component inside a navigator?"
70
73
  );
71
74
  }
72
75
  }
73
76
 
74
- const subscribe = React.useCallback(
75
- (callback: () => void) => {
76
- if (navigation) {
77
- return navigation.addListener('state', callback);
78
- } else if (stateListener) {
79
- return stateListener.subscribe(callback);
80
- } else {
81
- throw new Error(
82
- "Couldn't subscribe to navigation state changes. This is not expected."
83
- );
84
- }
85
- },
86
- [navigation, stateListener]
87
- );
77
+ const { getState, subscribe } = listener;
88
78
 
89
- const getSnapshot = navigation
90
- ? navigation.getState
91
- : stateListener?.getState;
79
+ const [, forceUpdate] = React.useReducer((count: number) => count + 1, 0);
92
80
 
93
- if (getSnapshot == null) {
94
- throw new Error("Couldn't get the navigation state. This is not expected.");
95
- }
81
+ const selected = select(getState());
96
82
 
97
- if (typeof selector !== 'function') {
98
- throw new Error(
99
- `A selector function must be provided (got ${typeof selector}).`
100
- );
101
- }
83
+ const selectionRef = React.useRef({ select, selected });
102
84
 
103
- const value = useSyncExternalStoreWithSelector(
104
- subscribe,
105
- getSnapshot,
106
- getSnapshot,
107
- // @ts-expect-error we can't infer the type here
108
- selector
109
- );
85
+ React.useLayoutEffect(() => {
86
+ selectionRef.current = { select, selected };
87
+ });
88
+
89
+ React.useEffect(() => {
90
+ const checkForUpdates = () => {
91
+ const selection = selectionRef.current;
110
92
 
111
- return value;
93
+ if (!Object.is(selection.selected, selection.select(getState()))) {
94
+ forceUpdate();
95
+ }
96
+ };
97
+
98
+ const unsubscribe = subscribe(checkForUpdates);
99
+
100
+ // The state may have changed between the render and the subscription
101
+ checkForUpdates();
102
+
103
+ return unsubscribe;
104
+ }, [getState, subscribe]);
105
+
106
+ return selected;
112
107
  }
113
108
 
114
109
  export function NavigationStateListenerProvider({
115
110
  state,
111
+ getState,
116
112
  children,
117
113
  }: {
118
114
  state: NavigationState<ParamListBase>;
115
+ getState: () => NavigationState<ParamListBase>;
119
116
  children: React.ReactNode;
120
117
  }) {
121
118
  const listeners = React.useRef<(() => void)[]>([]);
122
- const stateRef = React.useRef(state);
123
-
124
- const getState = useLatestCallback(() => stateRef.current);
125
119
 
126
120
  const subscribe = useLatestCallback((callback: () => void) => {
127
121
  listeners.current.push(callback);
@@ -131,8 +125,8 @@ export function NavigationStateListenerProvider({
131
125
  };
132
126
  });
133
127
 
128
+ // Notify subscribers once the new state has committed
134
129
  React.useLayoutEffect(() => {
135
- stateRef.current = state;
136
130
  listeners.current.forEach((callback) => callback());
137
131
  }, [state]);
138
132
 
@@ -151,6 +145,42 @@ export function NavigationStateListenerProvider({
151
145
  );
152
146
  }
153
147
 
148
+ export function NamedNavigationStateListenerProvider({
149
+ name,
150
+ children,
151
+ }: {
152
+ name: string;
153
+ children: React.ReactNode;
154
+ }) {
155
+ const listener = React.use(NavigationStateListenerContext);
156
+
157
+ if (listener == null) {
158
+ throw new Error(
159
+ "Couldn't find a navigation state listener. This is likely because the navigator doesn't render its content under 'NavigationContent'."
160
+ );
161
+ }
162
+
163
+ const parents = React.use(NamedNavigationStateListenerListContext);
164
+
165
+ const listeners = React.useMemo(
166
+ () => ({
167
+ ...parents,
168
+ [name]: listener,
169
+ }),
170
+ [listener, name, parents]
171
+ );
172
+
173
+ return (
174
+ <NamedNavigationStateListenerListContext.Provider value={listeners}>
175
+ {children}
176
+ </NamedNavigationStateListenerListContext.Provider>
177
+ );
178
+ }
179
+
154
180
  const NavigationStateListenerContext = React.createContext<
155
181
  NavigationStateListener | undefined
156
182
  >(undefined);
183
+
184
+ export const NamedNavigationStateListenerListContext = React.createContext<
185
+ Record<string, NavigationStateListener> | undefined
186
+ >(undefined);
@@ -1,17 +1,22 @@
1
1
  import type {
2
2
  NavigationAction,
3
+ NavigationRoute,
3
4
  NavigationState,
5
+ ParamListBase,
6
+ PartialRoute,
4
7
  PartialState,
5
8
  Router,
6
9
  RouterConfigOptions,
7
10
  } from '@react-navigation/routers';
8
11
  import * as React from 'react';
9
12
 
13
+ import { getLoaderForState } from './DataLoading';
10
14
  import {
11
15
  type ChildActionListener,
12
16
  type ChildBeforeRemoveListener,
13
17
  NavigationBuilderContext,
14
18
  } from './NavigationBuilderContext';
19
+ import { StaticTreeContext } from './StaticTreeContext';
15
20
  import type { EventMapCore } from './types';
16
21
  import type { NavigationEventEmitter } from './useEventEmitter';
17
22
  import { shouldPreventRemove, useOnPreventRemove } from './useOnPreventRemove';
@@ -27,6 +32,40 @@ type Options<State extends NavigationState> = {
27
32
  emitter: NavigationEventEmitter<EventMapCore<any>>;
28
33
  };
29
34
 
35
+ type MaybePartialRoute =
36
+ | NavigationRoute<ParamListBase, string>
37
+ | PartialRoute<NavigationRoute<ParamListBase, string>>;
38
+
39
+ const didFocusedRouteChange = (
40
+ currentState: NavigationState | PartialState<NavigationState>,
41
+ nextState: NavigationState | PartialState<NavigationState>
42
+ ) => {
43
+ let currentFocusedRoute: MaybePartialRoute | undefined =
44
+ currentState.routes[currentState.index ?? 0];
45
+
46
+ let nextFocusedRoute: MaybePartialRoute | undefined =
47
+ nextState.routes[nextState.index ?? 0];
48
+
49
+ let changed = currentFocusedRoute?.key !== nextFocusedRoute?.key;
50
+
51
+ while (
52
+ !changed &&
53
+ currentFocusedRoute?.state != null &&
54
+ nextFocusedRoute?.state != null &&
55
+ currentFocusedRoute.state !== nextFocusedRoute.state
56
+ ) {
57
+ currentFocusedRoute =
58
+ currentFocusedRoute.state.routes[currentFocusedRoute.state.index ?? 0];
59
+
60
+ nextFocusedRoute =
61
+ nextFocusedRoute.state.routes[nextFocusedRoute.state.index ?? 0];
62
+
63
+ changed = currentFocusedRoute?.key !== nextFocusedRoute?.key;
64
+ }
65
+
66
+ return changed;
67
+ };
68
+
30
69
  /**
31
70
  * Hook to handle actions for a navigator, including state updates and bubbling.
32
71
  *
@@ -53,10 +92,12 @@ export function useOnAction<State extends NavigationState>({
53
92
  onDispatchAction,
54
93
  } = React.use(NavigationBuilderContext);
55
94
 
95
+ const tree = React.use(StaticTreeContext);
96
+
56
97
  const routerConfigOptionsRef =
57
98
  React.useRef<RouterConfigOptions>(routerConfigOptions);
58
99
 
59
- React.useEffect(() => {
100
+ React.useInsertionEffect(() => {
60
101
  routerConfigOptionsRef.current = routerConfigOptions;
61
102
  });
62
103
 
@@ -105,6 +146,12 @@ export function useOnAction<State extends NavigationState>({
105
146
 
106
147
  onDispatchAction(action, false);
107
148
 
149
+ if (tree && didFocusedRouteChange(state, result)) {
150
+ const loader = getLoaderForState(tree, result);
151
+
152
+ loader?.();
153
+ }
154
+
108
155
  setState(result);
109
156
  } else {
110
157
  onDispatchAction(action, true);
@@ -155,6 +202,7 @@ export function useOnAction<State extends NavigationState>({
155
202
  onRouteFocusParent,
156
203
  router,
157
204
  setState,
205
+ tree,
158
206
  ]
159
207
  );
160
208
 
@@ -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
  }
@@ -4,17 +4,18 @@ import { NavigationBuilderContext } from './NavigationBuilderContext';
4
4
 
5
5
  /**
6
6
  * When screen config changes, we want to update the navigator in the same update phase.
7
- * However, navigation state is in the root component and React won't let us update it from a child.
8
- * This is a workaround for that, the scheduled update is stored in the ref without actually calling setState.
9
- * It lets all subsequent updates access the latest state so it stays correct.
10
- * Then we call setState during after the component updates.
7
+ * However, navigation state is in the root component and React won't let us update it from a child during render.
8
+ * The render could also be interrupted in concurrent mode, so we need to update after commit.
9
+ * So we queue the update in an insertion effect and flush it in a layout effect.
10
+ * Insertion effects run before layout effects, so every navigator in the tree has queued its update before we flush.
11
+ * This lets nested updates be applied from the root down without clobbering each other.
11
12
  */
12
13
  export function useScheduleUpdate(callback: () => void) {
13
14
  const { scheduleUpdate, flushUpdates } = React.use(NavigationBuilderContext);
14
15
 
15
- // FIXME: This is potentially unsafe
16
- // However, since we are using sync store, it might be fine
17
- scheduleUpdate(callback);
16
+ React.useInsertionEffect(() => {
17
+ scheduleUpdate(callback);
18
+ });
18
19
 
19
20
  React.useLayoutEffect(flushUpdates);
20
21
  }
@@ -100,7 +100,11 @@ export function useSyncState<T>(getInitialState: () => T) {
100
100
 
101
101
  if (pendingUpdates.length !== 0) {
102
102
  store.batchUpdates(() => {
103
- // Flush all the pending updates
103
+ // Updates are queued child-first as insertion effects run bottom-up.
104
+ // The root navigator replaces the whole state while nested navigators merge into it.
105
+ // So we apply them root-first to avoid the root clobbering nested updates.
106
+ pendingUpdates.reverse();
107
+
104
108
  for (const update of pendingUpdates) {
105
109
  update();
106
110
  }
package/src/utilities.tsx CHANGED
@@ -15,6 +15,17 @@ export type KeysOf<T> = T extends {} ? keyof T : never;
15
15
  */
16
16
  export type KeyOf<T extends {}> = Extract<keyof T, string>;
17
17
 
18
+ /**
19
+ * TS only checks object literals for excess properties when they are not generic.
20
+ * When an object is captured as a generic type, unknown keys are not caught.
21
+ * This builds a type from the provided object where any key not in the allowed type has the type `never`.
22
+ * Checking the provided object against this type then produces an error for each unknown key.
23
+ */
24
+ export type NoExcessObject<Provided, Allowed> = Record<
25
+ Exclude<keyof Provided, keyof Allowed>,
26
+ never
27
+ >;
28
+
18
29
  /**
19
30
  * We get a union type when using keyof, but we want an intersection instead.
20
31
  * https://stackoverflow.com/a/50375286/1665026
@@ -37,6 +48,23 @@ export type RequiredKeys<T extends object> = {
37
48
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
38
49
  }[keyof T];
39
50
 
51
+ type OptionalKeys<T extends object> = Exclude<keyof T, RequiredKeys<T>>;
52
+
53
+ /**
54
+ * Remove `| undefined` from the object type,
55
+ * and adjust the optionality of the keys based on the original type.
56
+ */
57
+ export type NotUndefinedObject<
58
+ T extends object | undefined,
59
+ O extends object = NotUndefined<T>,
60
+ > = undefined extends T
61
+ ? {
62
+ [K in RequiredKeys<O>]: O[K];
63
+ } & {
64
+ [K in OptionalKeys<O>]?: O[K] | undefined;
65
+ }
66
+ : T;
67
+
40
68
  export type UndefinedIfAllOptional<T> = T extends object
41
69
  ? RequiredKeys<T> extends never
42
70
  ? T | undefined
@@ -43,6 +43,7 @@ export function validatePathConfig(config: unknown, root = true) {
43
43
  }
44
44
 
45
45
  const pathConfig = config as Record<string, unknown>;
46
+
46
47
  let validationErrors: string[] | undefined;
47
48
 
48
49
  for (const key in pathConfig) {