@react-navigation/core 8.0.0-alpha.33 → 8.0.0-alpha.35

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/README.md +17 -14
  2. package/lib/module/BaseNavigationContainer.js +14 -2
  3. package/lib/module/BaseNavigationContainer.js.map +1 -1
  4. package/lib/module/NavigationBuilderContext.js +8 -13
  5. package/lib/module/NavigationBuilderContext.js.map +1 -1
  6. package/lib/module/NavigationIndependentTree.js +45 -15
  7. package/lib/module/NavigationIndependentTree.js.map +1 -1
  8. package/lib/module/NavigationProvider.js +9 -4
  9. package/lib/module/NavigationProvider.js.map +1 -1
  10. package/lib/module/PreventRemoveProvider.js +2 -2
  11. package/lib/module/PreventRemoveProvider.js.map +1 -1
  12. package/lib/module/SceneView.js +2 -1
  13. package/lib/module/SceneView.js.map +1 -1
  14. package/lib/module/getPathFromState.js +41 -16
  15. package/lib/module/getPathFromState.js.map +1 -1
  16. package/lib/module/getPatternParts.js +43 -12
  17. package/lib/module/getPatternParts.js.map +1 -1
  18. package/lib/module/getStateFromPath.js +175 -109
  19. package/lib/module/getStateFromPath.js.map +1 -1
  20. package/lib/module/index.js +1 -0
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/useDescriptors.js +9 -6
  23. package/lib/module/useDescriptors.js.map +1 -1
  24. package/lib/module/useFocusEffect.js +4 -2
  25. package/lib/module/useFocusEffect.js.map +1 -1
  26. package/lib/module/useFocusEvents.js +4 -2
  27. package/lib/module/useFocusEvents.js.map +1 -1
  28. package/lib/module/useFocusedListenersChildrenAdapter.js +2 -2
  29. package/lib/module/useFocusedListenersChildrenAdapter.js.map +1 -1
  30. package/lib/module/useIsFocused.js +1 -0
  31. package/lib/module/useIsFocused.js.map +1 -1
  32. package/lib/module/useNavigationBuilder.js +42 -39
  33. package/lib/module/useNavigationBuilder.js.map +1 -1
  34. package/lib/module/useNavigationCache.js +85 -81
  35. package/lib/module/useNavigationCache.js.map +1 -1
  36. package/lib/module/useNavigationHelpers.js +2 -2
  37. package/lib/module/useNavigationHelpers.js.map +1 -1
  38. package/lib/module/useNavigationState.js +48 -23
  39. package/lib/module/useNavigationState.js.map +1 -1
  40. package/lib/module/useOnAction.js +6 -4
  41. package/lib/module/useOnAction.js.map +1 -1
  42. package/lib/module/useOnGetNavigation.js +2 -2
  43. package/lib/module/useOnGetNavigation.js.map +1 -1
  44. package/lib/module/useOnGetState.js +2 -2
  45. package/lib/module/useOnGetState.js.map +1 -1
  46. package/lib/module/useOnPreventRemove.js +2 -2
  47. package/lib/module/useOnPreventRemove.js.map +1 -1
  48. package/lib/module/useOnRouteFocus.js +2 -2
  49. package/lib/module/useOnRouteFocus.js.map +1 -1
  50. package/lib/module/useOptionsGetters.js +19 -20
  51. package/lib/module/useOptionsGetters.js.map +1 -1
  52. package/lib/module/usePreventRemoveContext.js +1 -1
  53. package/lib/module/usePreventRemoveContext.js.map +1 -1
  54. package/lib/module/useRouteCache.js +43 -33
  55. package/lib/module/useRouteCache.js.map +1 -1
  56. package/lib/module/useScheduleUpdate.js +5 -3
  57. package/lib/module/useScheduleUpdate.js.map +1 -1
  58. package/lib/module/useSyncState.js +9 -10
  59. package/lib/module/useSyncState.js.map +1 -1
  60. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  61. package/lib/typescript/src/NavigationBuilderContext.d.ts +9 -6
  62. package/lib/typescript/src/NavigationBuilderContext.d.ts.map +1 -1
  63. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  64. package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
  65. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  66. package/lib/typescript/src/SceneView.d.ts +2 -1
  67. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  68. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  69. package/lib/typescript/src/getPatternParts.d.ts +4 -2
  70. package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
  71. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  72. package/lib/typescript/src/index.d.ts +1 -0
  73. package/lib/typescript/src/index.d.ts.map +1 -1
  74. package/lib/typescript/src/types.d.ts +1 -1
  75. package/lib/typescript/src/types.d.ts.map +1 -1
  76. package/lib/typescript/src/useDescriptors.d.ts +2 -2
  77. package/lib/typescript/src/useDescriptors.d.ts.map +1 -1
  78. package/lib/typescript/src/useFocusEffect.d.ts.map +1 -1
  79. package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
  80. package/lib/typescript/src/useIsFocused.d.ts +1 -0
  81. package/lib/typescript/src/useIsFocused.d.ts.map +1 -1
  82. package/lib/typescript/src/useNavigationBuilder.d.ts +1 -3
  83. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  84. package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
  85. package/lib/typescript/src/useNavigationState.d.ts +1 -0
  86. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  87. package/lib/typescript/src/useOnAction.d.ts.map +1 -1
  88. package/lib/typescript/src/useOnGetNavigation.d.ts.map +1 -1
  89. package/lib/typescript/src/useOnGetState.d.ts.map +1 -1
  90. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  91. package/lib/typescript/src/useOnRouteFocus.d.ts.map +1 -1
  92. package/lib/typescript/src/useOptionsGetters.d.ts +2 -4
  93. package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
  94. package/lib/typescript/src/useRouteCache.d.ts +7 -12
  95. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  96. package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
  97. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  98. package/lib/typescript/src/utilities.d.ts +3 -0
  99. package/lib/typescript/src/utilities.d.ts.map +1 -1
  100. package/package.json +2 -3
  101. package/src/BaseNavigationContainer.tsx +19 -2
  102. package/src/NavigationBuilderContext.tsx +22 -18
  103. package/src/NavigationIndependentTree.tsx +56 -22
  104. package/src/NavigationProvider.tsx +13 -4
  105. package/src/PreventRemoveProvider.tsx +2 -8
  106. package/src/SceneView.tsx +3 -1
  107. package/src/getPathFromState.tsx +63 -15
  108. package/src/getPatternParts.tsx +75 -14
  109. package/src/getStateFromPath.tsx +250 -113
  110. package/src/index.tsx +1 -0
  111. package/src/types.tsx +1 -1
  112. package/src/useDescriptors.tsx +20 -6
  113. package/src/useFocusEffect.tsx +5 -2
  114. package/src/useFocusEvents.tsx +5 -2
  115. package/src/useFocusedListenersChildrenAdapter.tsx +2 -2
  116. package/src/useIsFocused.tsx +4 -0
  117. package/src/useNavigationBuilder.tsx +54 -48
  118. package/src/useNavigationCache.tsx +130 -108
  119. package/src/useNavigationHelpers.tsx +2 -2
  120. package/src/useNavigationState.tsx +60 -29
  121. package/src/useOnAction.tsx +6 -5
  122. package/src/useOnGetNavigation.tsx +3 -2
  123. package/src/useOnGetState.tsx +4 -2
  124. package/src/useOnPreventRemove.tsx +4 -2
  125. package/src/useOnRouteFocus.tsx +2 -4
  126. package/src/useOptionsGetters.tsx +26 -25
  127. package/src/usePreventRemoveContext.tsx +1 -1
  128. package/src/useRouteCache.tsx +49 -33
  129. package/src/useScheduleUpdate.tsx +6 -3
  130. package/src/useSyncState.tsx +9 -11
  131. package/src/utilities.tsx +5 -2
  132. package/lib/module/useComponent.js +0 -34
  133. package/lib/module/useComponent.js.map +0 -1
  134. package/lib/typescript/src/useComponent.d.ts +0 -7
  135. package/lib/typescript/src/useComponent.d.ts.map +0 -1
  136. package/src/useComponent.tsx +0 -37
@@ -6,6 +6,7 @@ import type { NavigationListForNested, RootNavigator } from './types';
6
6
  type NavigationStateEvent = 'update' | 'commit';
7
7
 
8
8
  type NavigationStateListener = {
9
+ initialStateContext: React.Context<NavigationState<ParamListBase>>;
9
10
  getState: () => NavigationState<ParamListBase>;
10
11
  getSnapshot: () => NavigationState<ParamListBase>;
11
12
  subscribe: (callback: (event: NavigationStateEvent) => void) => () => void;
@@ -83,6 +84,25 @@ export function useNavigationState(...args: unknown[]): unknown {
83
84
 
84
85
  const { getState, getSnapshot, subscribe } = listener;
85
86
 
87
+ const initialRef = React.useRef(true);
88
+
89
+ // We read the initial state from context on mount instead of using getState
90
+ // This ensures that we get proper state for the current render,
91
+ // even if the external store has been updated.
92
+ // We conditionally use `use` to avoid more unnecessary re-renders after first mount.
93
+ // The context subscription is removed on the next render, which can cause one extra render.
94
+ const initialState = initialRef.current
95
+ ? React.use(listener.initialStateContext)
96
+ : undefined;
97
+
98
+ // We track the last committed reducer entry, navigation state, and selector,
99
+ // so we can check if an update changes the value the component last committed.
100
+ const committedRef = React.useRef<{
101
+ entry: NavigationStateEntry | null;
102
+ state: NavigationState<ParamListBase>;
103
+ select: (state: NavigationState<ParamListBase>) => unknown;
104
+ }>({ entry: null, state: initialState ?? getSnapshot(), select });
105
+
86
106
  // The store can contain state from a transition that hasn't committed yet.
87
107
  // Reading it during render can show state that doesn't match the navigator in that render.
88
108
  // The reducer lets React process state in the same render lane as the navigator.
@@ -92,11 +112,28 @@ export function useNavigationState(...args: unknown[]): unknown {
92
112
  // Reading here ensures the processed event uses the state for that render.
93
113
  const state = event === 'update' ? getState() : getSnapshot();
94
114
 
115
+ // If we're processing the same reducer entry as the last commit,
116
+ // we compare against the state last committed by the component.
117
+ // We keep the reducer entry to avoid re-renders for unchanged selections.
118
+ // This means the reducer entry can still hold older navigation state.
119
+ // If a parent causes another render,
120
+ // we use the latest snapshot so the selector doesn't read stale state.
121
+ // We leave the reducer entry unchanged to avoid an extra render.
122
+ // We save that snapshot in committedRef when the render commits,
123
+ // so later updates compare against the state we actually rendered,
124
+ // including when the selector changed.
125
+ // If the reducer entry is different, we use its own navigation state,
126
+ // so comparisons include updates already processed in the pending render.
127
+ const previousState =
128
+ previous === committedRef.current.entry
129
+ ? committedRef.current.state
130
+ : previous.state;
131
+
95
132
  if (
96
133
  previous.listener === listener &&
97
- (previous.state === state ||
134
+ (previousState === state ||
98
135
  (event === 'update' &&
99
- Object.is(select(previous.state), select(state))))
136
+ Object.is(select(previousState), select(state))))
100
137
  ) {
101
138
  return previous;
102
139
  }
@@ -104,37 +141,24 @@ export function useNavigationState(...args: unknown[]): unknown {
104
141
  return { listener, state };
105
142
  },
106
143
  undefined,
107
- () => ({ listener, state: getState() })
144
+ () => ({ listener, state: initialState ?? getSnapshot() })
108
145
  );
109
146
 
110
- let state = entry.state;
111
-
112
- const committedRef = React.useRef<{
113
- entry: NavigationStateEntry | null;
114
- state: NavigationState<ParamListBase>;
115
- select: (state: NavigationState<ParamListBase>) => unknown;
116
- }>({ entry: null, state, select });
147
+ const state =
148
+ // A different listener means the state belongs to another navigator.
149
+ // This can happen when the passed route name changes.
150
+ entry.listener !== listener ||
151
+ // An entry that advanced past the last commit is at least as new as the snapshot.
152
+ // An entry that didn't advance while the committed state changed is older.
153
+ entry === committedRef.current.entry
154
+ ? getSnapshot()
155
+ : entry.state;
117
156
 
118
157
  React.useInsertionEffect(() => {
158
+ initialRef.current = false;
119
159
  committedRef.current = { entry, state, select };
120
160
  });
121
161
 
122
- const snapshot = getSnapshot();
123
-
124
- const behind =
125
- state !== snapshot &&
126
- // A different listener means the state belongs to another navigator.
127
- // This can happen when the passed route name changes.
128
- (entry.listener !== listener ||
129
- // An entry that advanced past the last commit is at least as new as the snapshot.
130
- // An entry that didn't advance while the committed state changed is older.
131
- entry === committedRef.current.entry);
132
-
133
- if (behind) {
134
- state = snapshot;
135
- dispatch('commit');
136
- }
137
-
138
162
  React.useLayoutEffect(() => {
139
163
  const check = (event: NavigationStateEvent) => {
140
164
  const { state, select } = committedRef.current;
@@ -173,6 +197,10 @@ export function NavigationStateListenerProvider({
173
197
  subscribe: (callback: () => void) => () => void;
174
198
  children: React.ReactNode;
175
199
  }) {
200
+ const [InitialStateContext] = React.useState(() =>
201
+ React.createContext(state)
202
+ );
203
+
176
204
  const [listeners] = React.useState(
177
205
  () => new Set<(event: NavigationStateEvent) => void>()
178
206
  );
@@ -195,6 +223,7 @@ export function NavigationStateListenerProvider({
195
223
 
196
224
  const context = React.useMemo(
197
225
  () => ({
226
+ initialStateContext: InitialStateContext,
198
227
  getState,
199
228
  getSnapshot: () => nextState ?? snapshotRef.current,
200
229
  subscribe: (callback: (event: NavigationStateEvent) => void) => {
@@ -208,12 +237,14 @@ export function NavigationStateListenerProvider({
208
237
  };
209
238
  },
210
239
  }),
211
- [getState, listeners, nextState, subscribe]
240
+ [InitialStateContext, getState, listeners, nextState, subscribe]
212
241
  );
213
242
 
214
243
  return (
215
244
  <NavigationStateListenerContext.Provider value={context}>
216
- {children}
245
+ <InitialStateContext.Provider value={state}>
246
+ {children}
247
+ </InitialStateContext.Provider>
217
248
  </NavigationStateListenerContext.Provider>
218
249
  );
219
250
  }
@@ -229,7 +260,7 @@ export function NamedNavigationStateListenerProvider({
229
260
 
230
261
  if (listener == null) {
231
262
  throw new Error(
232
- "Couldn't find a navigation state listener. This is likely because the navigator doesn't render its content under 'NavigationContent'."
263
+ "Couldn't find a navigation state listener. This is likely a bug in the navigator.\n\nIf you're using a custom navigator, make sure that the navigator content is wrapped by the 'render' function returned by 'useNavigationBuilder'."
233
264
  );
234
265
  }
235
266
 
@@ -12,7 +12,7 @@ import { getLoaderForStateChange } from './DataLoading';
12
12
  import {
13
13
  type ChildActionListener,
14
14
  type ChildBeforeRemoveListener,
15
- NavigationBuilderContext,
15
+ useNavigationBuilderContext,
16
16
  } from './NavigationBuilderContext';
17
17
  import { StaticTreeContext } from './StaticTreeContext';
18
18
  import type { EventMapCore } from './types';
@@ -55,7 +55,7 @@ export function useOnAction<State extends NavigationState>({
55
55
  addListener: addListenerParent,
56
56
  onDispatchAction,
57
57
  flushUpdates,
58
- } = React.use(NavigationBuilderContext);
58
+ } = useNavigationBuilderContext();
59
59
 
60
60
  const tree = React.use(StaticTreeContext);
61
61
  const consumedParams = React.use(ConsumedParamsContext);
@@ -95,9 +95,10 @@ export function useOnAction<State extends NavigationState>({
95
95
  );
96
96
 
97
97
  // If a target is specified and set to current navigator, the action shouldn't bubble
98
- // So instead of `null`, we use the state object for such cases to signal that action was handled
99
- result =
100
- result === null && action.target === state.key ? state : result;
98
+ // So we immediately return `false` to mark it as unhandled
99
+ if (result === null && action.target === state.key) {
100
+ return false;
101
+ }
101
102
 
102
103
  if (result !== null && result.stale !== false) {
103
104
  // Some actions (e.g. `RESET`) may return a stale state from the router
@@ -1,7 +1,7 @@
1
1
  import type { ParamListBase } from '@react-navigation/routers';
2
2
  import * as React from 'react';
3
3
 
4
- import { NavigationBuilderContext } from './NavigationBuilderContext';
4
+ import { useNavigationBuilderContext } from './NavigationBuilderContext';
5
5
  import { NavigationRouteContext } from './NavigationProvider';
6
6
  import type { NavigationHelpers } from './types';
7
7
 
@@ -14,7 +14,8 @@ type Options = {
14
14
  * It's used for the navigation object returned by `useNavigation` outside a screen.
15
15
  */
16
16
  export function useOnGetNavigation({ navigation }: Options) {
17
- const { addKeyedListener } = React.use(NavigationBuilderContext);
17
+ const { addKeyedListener } = useNavigationBuilderContext();
18
+
18
19
  const route = React.use(NavigationRouteContext);
19
20
 
20
21
  React.useInsertionEffect(() => {
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
  import { isArrayEqual } from './isArrayEqual';
5
5
  import {
6
6
  type GetStateListener,
7
- NavigationBuilderContext,
7
+ useNavigationBuilderContext,
8
8
  } from './NavigationBuilderContext';
9
9
  import { NavigationRouteContext } from './NavigationProvider';
10
10
 
@@ -14,8 +14,10 @@ type Options = {
14
14
  };
15
15
 
16
16
  export function useOnGetState({ getState, getStateListeners }: Options) {
17
- const { addKeyedListener } = React.use(NavigationBuilderContext);
17
+ const { addKeyedListener } = useNavigationBuilderContext();
18
+
18
19
  const route = React.use(NavigationRouteContext);
20
+
19
21
  const key = route ? route.key : 'root';
20
22
 
21
23
  const getRehydratedState = React.useCallback(() => {
@@ -9,7 +9,7 @@ import * as React from 'react';
9
9
 
10
10
  import {
11
11
  type ChildBeforeRemoveListener,
12
- NavigationBuilderContext,
12
+ useNavigationBuilderContext,
13
13
  } from './NavigationBuilderContext';
14
14
  import { NavigationRouteContext } from './NavigationProvider';
15
15
  import type { EventMapCore } from './types';
@@ -107,8 +107,10 @@ export function useOnPreventRemove({
107
107
  emitter,
108
108
  beforeRemoveListeners,
109
109
  }: Options) {
110
- const { addKeyedListener } = React.use(NavigationBuilderContext);
110
+ const { addKeyedListener } = useNavigationBuilderContext();
111
+
111
112
  const route = React.use(NavigationRouteContext);
113
+
112
114
  const routeKey = route?.key;
113
115
 
114
116
  React.useInsertionEffect(() => {
@@ -5,7 +5,7 @@ import type {
5
5
  } from '@react-navigation/routers';
6
6
  import * as React from 'react';
7
7
 
8
- import { NavigationBuilderContext } from './NavigationBuilderContext';
8
+ import { useNavigationBuilderContext } from './NavigationBuilderContext';
9
9
 
10
10
  type Options<State extends NavigationState, Action extends NavigationAction> = {
11
11
  router: Router<State, Action>;
@@ -23,9 +23,7 @@ export function useOnRouteFocus<
23
23
  State extends NavigationState,
24
24
  Action extends NavigationAction,
25
25
  >({ router, getState, key: sourceRouteKey, setState }: Options<State, Action>) {
26
- const { onRouteFocus: onRouteFocusParent } = React.use(
27
- NavigationBuilderContext
28
- );
26
+ const { onRouteFocus: onRouteFocusParent } = useNavigationBuilderContext();
29
27
 
30
28
  return React.useCallback(
31
29
  (key: string) => {
@@ -1,45 +1,40 @@
1
- import type { ParamListBase } from '@react-navigation/routers';
2
1
  import * as React from 'react';
3
2
 
4
3
  import { NavigationBuilderContext } from './NavigationBuilderContext';
5
4
  import { NavigationStateContext } from './NavigationStateContext';
6
- import type { NavigationProp } from './types';
5
+ import { IsFocusedContext } from './useIsFocused';
7
6
 
8
7
  type Options = {
9
8
  key?: string;
10
- navigation?: NavigationProp<ParamListBase>;
11
9
  options?: object | undefined;
10
+ getFocusedRouteKey?: () => string | undefined;
12
11
  };
13
12
 
14
- export function useOptionsGetters({ key, options, navigation }: Options) {
13
+ export function useOptionsGetters({
14
+ key,
15
+ options,
16
+ getFocusedRouteKey,
17
+ }: Options) {
18
+ const isFocused = React.use(IsFocusedContext) ?? true;
19
+
15
20
  const optionsRef = React.useRef<object | undefined>(options);
16
21
  const optionsGettersFromChildRef = React.useRef<
17
22
  Record<string, () => object | undefined | null>
18
23
  >({});
19
24
 
20
- const { onOptionsChange } = React.use(NavigationBuilderContext);
25
+ const onOptionsChange = React.use(NavigationBuilderContext)?.onOptionsChange;
26
+
21
27
  const { addOptionsGetter: parentAddOptionsGetter } = React.use(
22
28
  NavigationStateContext
23
29
  );
24
30
 
25
- const optionsChangeListener = React.useCallback(() => {
26
- const isFocused = navigation?.isFocused() ?? true;
27
- const hasChildren = Object.keys(optionsGettersFromChildRef.current).length;
28
-
29
- if (isFocused && !hasChildren) {
30
- onOptionsChange(optionsRef.current ?? {});
31
- }
32
- }, [navigation, onOptionsChange]);
33
-
34
31
  React.useInsertionEffect(() => {
35
32
  optionsRef.current = options;
36
33
  }, [options]);
37
34
 
38
35
  React.useEffect(() => {
39
- optionsChangeListener();
40
-
41
- return navigation?.addListener('focus', optionsChangeListener);
42
- }, [navigation, options, optionsChangeListener]);
36
+ onOptionsChange?.();
37
+ }, [isFocused, onOptionsChange, options]);
43
38
 
44
39
  const getOptionsFromListener = React.useCallback(() => {
45
40
  for (const key in optionsGettersFromChildRef.current) {
@@ -57,9 +52,11 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
57
52
  }, []);
58
53
 
59
54
  const getCurrentOptions = React.useCallback(() => {
60
- const isFocused = navigation?.isFocused() ?? true;
61
-
62
- if (!isFocused) {
55
+ // We use focused route key from the navigator instead of `isFocused`
56
+ // Because `isFocused` may reflect the value the screen last rendered with,
57
+ // e.g. if `Activity` hides the screen before it rendered the new value,
58
+ // it updates at a lower priority, so it may be delayed.
59
+ if (key != null && getFocusedRouteKey?.() !== key) {
63
60
  return null;
64
61
  }
65
62
 
@@ -70,7 +67,7 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
70
67
  }
71
68
 
72
69
  return optionsRef.current;
73
- }, [navigation, getOptionsFromListener]);
70
+ }, [key, getFocusedRouteKey, getOptionsFromListener]);
74
71
 
75
72
  React.useInsertionEffect(() => {
76
73
  // We don't have a parent at the root
@@ -82,15 +79,19 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
82
79
  const addOptionsGetter = React.useCallback(
83
80
  (key: string, getter: () => object | undefined | null) => {
84
81
  optionsGettersFromChildRef.current[key] = getter;
85
- optionsChangeListener();
86
82
 
87
83
  return () => {
88
84
  // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
89
85
  delete optionsGettersFromChildRef.current[key];
90
- optionsChangeListener();
86
+
87
+ // Other getters and the focused route may still change during cleanup.
88
+ // Wait until the commit finishes before reporting the remaining options.
89
+ if (onOptionsChange !== undefined) {
90
+ queueMicrotask(onOptionsChange);
91
+ }
91
92
  };
92
93
  },
93
- [optionsChangeListener]
94
+ [onOptionsChange]
94
95
  );
95
96
 
96
97
  return {
@@ -7,7 +7,7 @@ export function usePreventRemoveContext() {
7
7
 
8
8
  if (value == null) {
9
9
  throw new Error(
10
- "Couldn't find the prevent remove context. Is your component inside NavigationContent?"
10
+ "Couldn't find the prevent remove context. This is likely a bug in the navigator.\n\nIf you're using a custom navigator, make sure that the navigator content is wrapped by the 'render' function returned by 'useNavigationBuilder'."
11
11
  );
12
12
  }
13
13
 
@@ -11,6 +11,10 @@ import type { RouteProp } from './types';
11
11
  */
12
12
  export const CHILD_STATE = Symbol('CHILD_STATE');
13
13
 
14
+ type CachedRoute = RouteProp<ParamListBase> & {
15
+ [CHILD_STATE]?: NavigationState;
16
+ };
17
+
14
18
  /**
15
19
  * Hook to cache route props for each screen in the navigator.
16
20
  * This lets add warnings and modifications to the route object but keep references between renders.
@@ -20,38 +24,58 @@ export function useRouteCache<State extends NavigationState>(
20
24
  ) {
21
25
  // Cache object which holds route objects for each screen
22
26
  const cache = React.useMemo(
23
- () => ({ current: new Map<string, RouteProp<ParamListBase>>() }),
27
+ () => ({ current: new Map<string, CachedRoute>() }),
24
28
  []
25
29
  );
26
30
 
27
- const next = routes.reduce((acc, route) => {
28
- const previous = cache.current.get(route.key);
29
- const { state, ...routeWithoutState } = route;
31
+ const next = React.useMemo(() => {
32
+ return routes.reduce((acc, route) => {
33
+ const previous = cache.current.get(route.key);
30
34
 
31
- let proxy;
35
+ const { state: _, ...routeWithoutState } = route;
32
36
 
33
- if (previous && isRecordEqual(previous, routeWithoutState)) {
34
- // If a cached route object already exists, reuse it
35
- proxy = previous;
36
- } else {
37
- proxy = routeWithoutState;
38
- }
37
+ let proxy: CachedRoute;
39
38
 
40
- if (process.env.NODE_ENV !== 'production' && proxy !== previous) {
41
- // FIXME: since the state is updated with mutation, the route object cannot be frozen
42
- // As a workaround, loop through the object and make the properties readonly
43
- // Only needed once per proxy - skip if we're reusing a previously-frozen one
44
- for (const [key, value] of Object.entries(proxy)) {
45
- Object.defineProperty(proxy, key, {
46
- enumerable: true,
47
- configurable: true,
48
- writable: false,
49
- value,
50
- });
39
+ if (previous && isRecordEqual(previous, routeWithoutState)) {
40
+ // If a cached route object already exists, reuse it
41
+ proxy = previous;
42
+ } else {
43
+ proxy = routeWithoutState;
44
+
45
+ if (process.env.NODE_ENV !== 'production') {
46
+ // FIXME: since the state is updated with mutation, the route object cannot be frozen
47
+ // As a workaround, loop through the object and make the properties readonly
48
+ // Only needed once per proxy - skip if we're reusing a previously-frozen one
49
+ for (const [key, value] of Object.entries(proxy)) {
50
+ Object.defineProperty(proxy, key, {
51
+ enumerable: true,
52
+ configurable: true,
53
+ writable: false,
54
+ value,
55
+ });
56
+ }
57
+ }
51
58
  }
52
- }
53
59
 
54
- // @ts-expect-error: this isn't in type definitions coz we want this private
60
+ acc.set(route.key, proxy);
61
+
62
+ return acc;
63
+ }, new Map<string, CachedRoute>());
64
+ }, [cache, routes]);
65
+
66
+ React.useInsertionEffect(() => {
67
+ cache.current = next;
68
+ });
69
+
70
+ // Update child state on every render, including when the memo is reused.
71
+ // This restores the previous child state when a pending render is canceled.
72
+ return Array.from(next.values(), (proxy, index) => {
73
+ const state = routes[index]?.state;
74
+
75
+ // FIXME: We mutate the child state without replacing the cached route object,
76
+ // so nested navigation doesn't re-render the parent screen.
77
+ // This is not concurrent render safe, pending renders mutate this shared child state.
78
+ // Then `getFocusedRouteNameFromRoute` can expose it before the render commits.
55
79
  if (proxy[CHILD_STATE] !== state) {
56
80
  Object.defineProperty(proxy, CHILD_STATE, {
57
81
  enumerable: false,
@@ -60,14 +84,6 @@ export function useRouteCache<State extends NavigationState>(
60
84
  });
61
85
  }
62
86
 
63
- acc.set(route.key, proxy);
64
-
65
- return acc;
66
- }, new Map<string, RouteProp<ParamListBase>>());
67
-
68
- React.useInsertionEffect(() => {
69
- cache.current = next;
87
+ return proxy;
70
88
  });
71
-
72
- return Array.from(next.values());
73
89
  }
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
+ import useLatestCallback from 'use-latest-callback';
2
3
 
3
- import { NavigationBuilderContext } from './NavigationBuilderContext';
4
+ import { useNavigationBuilderContext } from './NavigationBuilderContext';
4
5
 
5
6
  /**
6
7
  * When screen config changes, we want to update the navigator in the same update phase.
@@ -11,10 +12,12 @@ import { NavigationBuilderContext } from './NavigationBuilderContext';
11
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
- const { scheduleUpdate, flushUpdates } = React.use(NavigationBuilderContext);
15
+ const { scheduleUpdate, flushUpdates } = useNavigationBuilderContext();
16
+
17
+ const latestCallback = useLatestCallback(callback);
15
18
 
16
19
  React.useInsertionEffect(() => {
17
- scheduleUpdate(callback);
20
+ scheduleUpdate(latestCallback);
18
21
  });
19
22
 
20
23
  React.useLayoutEffect(flushUpdates);
@@ -108,25 +108,23 @@ export function useSyncState<T>(getInitialState: () => T) {
108
108
  return unsubscribe;
109
109
  }, [store]);
110
110
 
111
- const pendingUpdatesRef = React.useRef<(() => void)[]>([]);
111
+ const pendingUpdates = useLazyValue(() => new Set<() => void>());
112
112
 
113
113
  const scheduleUpdate = useLatestCallback((callback: () => void) => {
114
- pendingUpdatesRef.current.push(callback);
114
+ pendingUpdates.add(callback);
115
115
  });
116
116
 
117
117
  const flushUpdates = useLatestCallback(() => {
118
- const pendingUpdates = pendingUpdatesRef.current;
118
+ if (pendingUpdates.size !== 0) {
119
+ // Updates are queued child-first as insertion effects run bottom-up.
120
+ // The root navigator replaces the whole state while nested navigators merge into it.
121
+ // So we apply them root-first to avoid the root clobbering nested updates.
122
+ const callbacks = Array.from(pendingUpdates).reverse();
119
123
 
120
- pendingUpdatesRef.current = [];
124
+ pendingUpdates.clear();
121
125
 
122
- if (pendingUpdates.length !== 0) {
123
126
  store.batchUpdates(() => {
124
- // Updates are queued child-first as insertion effects run bottom-up.
125
- // The root navigator replaces the whole state while nested navigators merge into it.
126
- // So we apply them root-first to avoid the root clobbering nested updates.
127
- pendingUpdates.reverse();
128
-
129
- for (const update of pendingUpdates) {
127
+ for (const update of callbacks) {
130
128
  update();
131
129
  }
132
130
  });
package/src/utilities.tsx CHANGED
@@ -120,9 +120,11 @@ type StripRegex<Param extends string> = Param extends `${infer Name}(${string})`
120
120
  type ExtractSegmentParam<Segment extends string> =
121
121
  Segment extends `:${infer Param}?`
122
122
  ? { [K in StripRegex<Param>]?: string }
123
- : Segment extends `:${infer Param}`
123
+ : Segment extends `:${infer Param}${'+' | '*'}`
124
124
  ? { [K in StripRegex<Param>]: string }
125
- : {};
125
+ : Segment extends `:${infer Param}`
126
+ ? { [K in StripRegex<Param>]: string }
127
+ : {};
126
128
 
127
129
  export type StandardSchemaValidationResult<Output> =
128
130
  | { value: Output; issues?: undefined }
@@ -146,6 +148,7 @@ export type QueryParamInput = string | string[] | null | undefined;
146
148
  * Extract path params from a path string.
147
149
  * e.g. `/foo/:userId/:postId` -> `{ userId: string; postId: string }`
148
150
  * Supports optional params with `?` suffix.
151
+ * Supports repeated params with `+` and `*` suffixes as strings.
149
152
  * Params must start with `:` at the beginning of a segment (after `/`).
150
153
  */
151
154
  export type ExtractParamStrings<Path extends string> =
@@ -1,34 +0,0 @@
1
- "use strict";
2
-
3
- import * as React from 'react';
4
- import { jsx as _jsx } from "react/jsx-runtime";
5
- const NavigationContent = ({
6
- render,
7
- children
8
- }) => {
9
- return render(children);
10
- };
11
- export function useComponent(render) {
12
- const renderRef = React.useRef(render);
13
-
14
- // Normally refs shouldn't be mutated in render
15
- // But we return a component which will be rendered
16
- // So it's just for immediate consumption
17
- renderRef.current = render;
18
- React.useEffect(() => {
19
- renderRef.current = null;
20
- });
21
- return React.useRef(({
22
- children
23
- }) => {
24
- const render = renderRef.current;
25
- if (render === null) {
26
- throw new Error('The returned component must be rendered in the same render phase as the hook.');
27
- }
28
- return /*#__PURE__*/_jsx(NavigationContent, {
29
- render: render,
30
- children: children
31
- });
32
- }).current;
33
- }
34
- //# sourceMappingURL=useComponent.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["React","jsx","_jsx","NavigationContent","render","children","useComponent","renderRef","useRef","current","useEffect","Error"],"sourceRoot":"../../src","sources":["useComponent.tsx"],"mappings":";;AAAA,OAAO,KAAKA,KAAK,MAAM,OAAO;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAS/B,MAAMC,iBAAiB,GAAGA,CAAC;EAAEC,MAAM;EAAEC;AAAgB,CAAC,KAAK;EACzD,OAAOD,MAAM,CAACC,QAAQ,CAAC;AACzB,CAAC;AAED,OAAO,SAASC,YAAYA,CAACF,MAAc,EAAE;EAC3C,MAAMG,SAAS,GAAGP,KAAK,CAACQ,MAAM,CAAgBJ,MAAM,CAAC;;EAErD;EACA;EACA;EACAG,SAAS,CAACE,OAAO,GAAGL,MAAM;EAE1BJ,KAAK,CAACU,SAAS,CAAC,MAAM;IACpBH,SAAS,CAACE,OAAO,GAAG,IAAI;EAC1B,CAAC,CAAC;EAEF,OAAOT,KAAK,CAACQ,MAAM,CAAC,CAAC;IAAEH;EAAwC,CAAC,KAAK;IACnE,MAAMD,MAAM,GAAGG,SAAS,CAACE,OAAO;IAEhC,IAAIL,MAAM,KAAK,IAAI,EAAE;MACnB,MAAM,IAAIO,KAAK,CACb,+EACF,CAAC;IACH;IAEA,oBAAOT,IAAA,CAACC,iBAAiB;MAACC,MAAM,EAAEA,MAAO;MAAAC,QAAA,EAAEA;IAAQ,CAAoB,CAAC;EAC1E,CAAC,CAAC,CAACI,OAAO;AACZ","ignoreList":[]}
@@ -1,7 +0,0 @@
1
- import * as React from 'react';
2
- type Render = (children: React.ReactNode) => React.JSX.Element;
3
- export declare function useComponent(render: Render): ({ children }: {
4
- children: React.ReactNode;
5
- }) => React.JSX.Element;
6
- export {};
7
- //# sourceMappingURL=useComponent.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useComponent.d.ts","sourceRoot":"","sources":["../../../src/useComponent.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,KAAK,MAAM,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;AAW/D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,kBAYN;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,uBAWjE"}