@react-navigation/core 8.0.0-alpha.34 → 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 (84) 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/NavigationIndependentTree.js +9 -6
  5. package/lib/module/NavigationIndependentTree.js.map +1 -1
  6. package/lib/module/NavigationProvider.js +9 -4
  7. package/lib/module/NavigationProvider.js.map +1 -1
  8. package/lib/module/PreventRemoveProvider.js +2 -2
  9. package/lib/module/PreventRemoveProvider.js.map +1 -1
  10. package/lib/module/SceneView.js +2 -1
  11. package/lib/module/SceneView.js.map +1 -1
  12. package/lib/module/useDescriptors.js +7 -4
  13. package/lib/module/useDescriptors.js.map +1 -1
  14. package/lib/module/useFocusEffect.js +4 -2
  15. package/lib/module/useFocusEffect.js.map +1 -1
  16. package/lib/module/useFocusEvents.js +4 -2
  17. package/lib/module/useFocusEvents.js.map +1 -1
  18. package/lib/module/useIsFocused.js +1 -0
  19. package/lib/module/useIsFocused.js.map +1 -1
  20. package/lib/module/useNavigationBuilder.js +40 -37
  21. package/lib/module/useNavigationBuilder.js.map +1 -1
  22. package/lib/module/useNavigationCache.js +82 -78
  23. package/lib/module/useNavigationCache.js.map +1 -1
  24. package/lib/module/useNavigationState.js +48 -23
  25. package/lib/module/useNavigationState.js.map +1 -1
  26. package/lib/module/useOptionsGetters.js +18 -17
  27. package/lib/module/useOptionsGetters.js.map +1 -1
  28. package/lib/module/usePreventRemoveContext.js +1 -1
  29. package/lib/module/usePreventRemoveContext.js.map +1 -1
  30. package/lib/module/useRouteCache.js +43 -33
  31. package/lib/module/useRouteCache.js.map +1 -1
  32. package/lib/module/useScheduleUpdate.js +3 -1
  33. package/lib/module/useScheduleUpdate.js.map +1 -1
  34. package/lib/module/useSyncState.js +9 -10
  35. package/lib/module/useSyncState.js.map +1 -1
  36. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  37. package/lib/typescript/src/NavigationBuilderContext.d.ts +1 -1
  38. package/lib/typescript/src/NavigationBuilderContext.d.ts.map +1 -1
  39. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  40. package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
  41. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  42. package/lib/typescript/src/SceneView.d.ts +2 -1
  43. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  44. package/lib/typescript/src/useDescriptors.d.ts +2 -2
  45. package/lib/typescript/src/useDescriptors.d.ts.map +1 -1
  46. package/lib/typescript/src/useFocusEffect.d.ts.map +1 -1
  47. package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
  48. package/lib/typescript/src/useIsFocused.d.ts +1 -0
  49. package/lib/typescript/src/useIsFocused.d.ts.map +1 -1
  50. package/lib/typescript/src/useNavigationBuilder.d.ts +1 -3
  51. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  52. package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
  53. package/lib/typescript/src/useNavigationState.d.ts +1 -0
  54. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  55. package/lib/typescript/src/useOptionsGetters.d.ts +2 -4
  56. package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
  57. package/lib/typescript/src/useRouteCache.d.ts +7 -12
  58. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  59. package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
  60. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  61. package/package.json +2 -2
  62. package/src/BaseNavigationContainer.tsx +19 -2
  63. package/src/NavigationBuilderContext.tsx +1 -1
  64. package/src/NavigationIndependentTree.tsx +16 -8
  65. package/src/NavigationProvider.tsx +13 -4
  66. package/src/PreventRemoveProvider.tsx +2 -8
  67. package/src/SceneView.tsx +3 -1
  68. package/src/useDescriptors.tsx +18 -5
  69. package/src/useFocusEffect.tsx +5 -2
  70. package/src/useFocusEvents.tsx +5 -2
  71. package/src/useIsFocused.tsx +4 -0
  72. package/src/useNavigationBuilder.tsx +53 -45
  73. package/src/useNavigationCache.tsx +127 -105
  74. package/src/useNavigationState.tsx +60 -29
  75. package/src/useOptionsGetters.tsx +24 -24
  76. package/src/usePreventRemoveContext.tsx +1 -1
  77. package/src/useRouteCache.tsx +49 -33
  78. package/src/useScheduleUpdate.tsx +4 -1
  79. package/src/useSyncState.tsx +9 -11
  80. package/lib/module/useComponent.js +0 -34
  81. package/lib/module/useComponent.js.map +0 -1
  82. package/lib/typescript/src/useComponent.d.ts +0 -7
  83. package/lib/typescript/src/useComponent.d.ts.map +0 -1
  84. package/src/useComponent.tsx +0 -37
@@ -79,121 +79,143 @@ export function useNavigationCache<
79
79
  // Cache object which holds navigation objects for each screen
80
80
  // We use `React.useMemo` instead of `React.useRef` coz we want to invalidate it when deps change
81
81
  // In reality, these deps will rarely change, if ever
82
- const cache = React.useMemo(
83
- () => ({
84
- current: {} as NavigationCache<
85
- State,
86
- ScreenOptions,
87
- EventMap,
88
- ActionHelpers
89
- >,
90
- }),
82
+ const cache = React.useMemo<{
83
+ current: NavigationCache<State, ScreenOptions, EventMap, ActionHelpers>;
84
+ routeNames: Record<string, string>;
85
+ }>(
86
+ () => ({ current: {}, routeNames: {} }),
91
87
  // eslint-disable-next-line react-hooks/exhaustive-deps
92
- [getState, parentNavigation, navigation, setOptions, emitter]
88
+ [
89
+ getState,
90
+ parentNavigation,
91
+ navigation,
92
+ setOptions,
93
+ router,
94
+ emitter,
95
+ withStackTrace,
96
+ ]
93
97
  );
94
98
 
95
- const next = routes.reduce<
96
- NavigationCache<State, ScreenOptions, EventMap, ActionHelpers>
97
- >((acc, route) => {
98
- const previous = cache.current[route.key];
99
-
100
- if (previous) {
101
- // If a cached navigation object already exists, reuse it
102
- acc[route.key] = previous;
103
- } else {
104
- const dispatch = (
105
- thunk:
106
- | NavigationAction
107
- | ((state: State) => NavigationAction | null | undefined)
108
- ) => {
109
- withStackTrace(dispatch, () => {
110
- React.startTransition(() => {
111
- const action =
112
- typeof thunk === 'function' ? thunk(getState()) : thunk;
113
-
114
- if (action != null) {
115
- navigation.dispatch({ source: route.key, ...action });
116
- }
99
+ const next = React.useMemo(() => {
100
+ return routes.reduce<
101
+ NavigationCache<State, ScreenOptions, EventMap, ActionHelpers>
102
+ >((acc, route) => {
103
+ const previous =
104
+ cache.routeNames[route.key] === route.name
105
+ ? cache.current[route.key]
106
+ : undefined;
107
+
108
+ if (previous) {
109
+ // If a cached navigation object already exists, reuse it
110
+ acc[route.key] = previous;
111
+ } else {
112
+ const dispatch = (
113
+ thunk:
114
+ | NavigationAction
115
+ | ((state: State) => NavigationAction | null | undefined)
116
+ ) => {
117
+ withStackTrace(dispatch, () => {
118
+ React.startTransition(() => {
119
+ const action =
120
+ typeof thunk === 'function' ? thunk(getState()) : thunk;
121
+
122
+ if (action != null) {
123
+ navigation.dispatch({ source: route.key, ...action });
124
+ }
125
+ });
117
126
  });
118
- });
119
- };
120
-
121
- const actions = {
122
- ...router.actionCreators,
123
- ...CommonActions,
124
- };
125
-
126
- const helpers = Object.keys(actions).reduce<Record<string, () => void>>(
127
- (acc, name) => {
128
- const helper = (...args: any) =>
129
- withStackTrace(helper, () =>
130
- // @ts-expect-error: name is a valid key, but TypeScript is dumb
131
- dispatch(actions[name](...args))
132
- );
133
-
134
- acc[name] = helper;
135
-
136
- return acc;
137
- },
138
- {}
139
- );
140
-
141
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
142
- const { emit, ...rest } = navigation;
143
-
144
- acc[route.key] = {
145
- ...rest,
146
- ...helpers,
147
- // FIXME: too much work to fix the types for now
148
- ...(emitter.create(route.key) as any),
149
- dispatch,
150
- getParent: (routeName) => {
151
- if (routeName === route.name) {
152
- // If the passed route name is the same as the current route's name,
153
- // we return the cached navigation object for the route
154
- return acc[route.key];
155
- }
156
-
157
- if (routeName !== undefined) {
158
- const parent = parentNavigation?.getParent(routeName);
159
-
160
- if (parent === undefined) {
161
- throw new Error(
162
- `Couldn't find a navigation object for '${routeName}' in current or any parent screens. Is your component inside the correct screen?`
127
+ };
128
+
129
+ const actions = {
130
+ ...router.actionCreators,
131
+ ...CommonActions,
132
+ };
133
+
134
+ const helpers = Object.keys(actions).reduce<Record<string, () => void>>(
135
+ (acc, name) => {
136
+ const helper = (...args: any) =>
137
+ withStackTrace(helper, () =>
138
+ // @ts-expect-error: name is a valid key, but TypeScript is dumb
139
+ dispatch(actions[name](...args))
163
140
  );
141
+
142
+ acc[name] = helper;
143
+
144
+ return acc;
145
+ },
146
+ {}
147
+ );
148
+
149
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
150
+ const { emit, ...rest } = navigation;
151
+
152
+ acc[route.key] = {
153
+ ...rest,
154
+ ...helpers,
155
+ // FIXME: too much work to fix the types for now
156
+ ...(emitter.create(route.key) as any),
157
+ dispatch,
158
+ getParent: (routeName) => {
159
+ if (routeName === route.name) {
160
+ // If the passed route name is the same as the current route's name,
161
+ // we return the cached navigation object for the route
162
+ return acc[route.key];
163
+ }
164
+
165
+ if (routeName !== undefined) {
166
+ const parent = parentNavigation?.getParent(routeName);
167
+
168
+ if (parent === undefined) {
169
+ throw new Error(
170
+ `Couldn't find a navigation object for '${routeName}' in current or any parent screens. Is your component inside the correct screen?`
171
+ );
172
+ }
173
+
174
+ return parent;
175
+ }
176
+
177
+ return parentNavigation;
178
+ },
179
+ setOptions: (options: Partial<ScreenOptions>) => {
180
+ setOptions((o) => ({
181
+ ...o,
182
+ [route.key]: { ...o[route.key], ...options },
183
+ }));
184
+ },
185
+ isFocused: () => {
186
+ const state = rest.getState();
187
+
188
+ if (state.routes[state.index]?.key !== route.key) {
189
+ return false;
164
190
  }
165
191
 
166
- return parent;
167
- }
168
-
169
- return parentNavigation;
170
- },
171
- setOptions: (options: Partial<ScreenOptions>) => {
172
- setOptions((o) => ({
173
- ...o,
174
- [route.key]: { ...o[route.key], ...options },
175
- }));
176
- },
177
- isFocused: () => {
178
- const state = rest.getState();
179
-
180
- if (state.routes[state.index]?.key !== route.key) {
181
- return false;
182
- }
183
-
184
- // If the current screen is focused, we also need to check if parent navigator is focused
185
- // This makes sure that we return the focus state in the whole tree, not just this navigator
186
- return navigation ? navigation.isFocused() : true;
187
- },
188
- };
189
- }
190
-
191
- return acc;
192
- }, {});
192
+ // If the current screen is focused, we also need to check if parent navigator is focused
193
+ // This makes sure that we return the focus state in the whole tree, not just this navigator
194
+ return navigation ? navigation.isFocused() : true;
195
+ },
196
+ };
197
+ }
198
+
199
+ return acc;
200
+ }, {});
201
+ }, [
202
+ cache,
203
+ routes,
204
+ getState,
205
+ navigation,
206
+ router,
207
+ withStackTrace,
208
+ emitter,
209
+ parentNavigation,
210
+ setOptions,
211
+ ]);
193
212
 
194
213
  React.useInsertionEffect(() => {
195
214
  cache.current = next;
196
- });
215
+ cache.routeNames = Object.fromEntries(
216
+ routes.map((route) => [route.key, route.name])
217
+ );
218
+ }, [cache, next, routes]);
197
219
 
198
220
  return next;
199
221
  }
@@ -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
 
@@ -1,17 +1,22 @@
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>
@@ -23,24 +28,13 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
23
28
  NavigationStateContext
24
29
  );
25
30
 
26
- const optionsChangeListener = React.useCallback(() => {
27
- const isFocused = navigation?.isFocused() ?? true;
28
- const hasChildren = Object.keys(optionsGettersFromChildRef.current).length;
29
-
30
- if (isFocused && !hasChildren) {
31
- onOptionsChange?.(optionsRef.current ?? {});
32
- }
33
- }, [navigation, onOptionsChange]);
34
-
35
31
  React.useInsertionEffect(() => {
36
32
  optionsRef.current = options;
37
33
  }, [options]);
38
34
 
39
35
  React.useEffect(() => {
40
- optionsChangeListener();
41
-
42
- return navigation?.addListener('focus', optionsChangeListener);
43
- }, [navigation, options, optionsChangeListener]);
36
+ onOptionsChange?.();
37
+ }, [isFocused, onOptionsChange, options]);
44
38
 
45
39
  const getOptionsFromListener = React.useCallback(() => {
46
40
  for (const key in optionsGettersFromChildRef.current) {
@@ -58,9 +52,11 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
58
52
  }, []);
59
53
 
60
54
  const getCurrentOptions = React.useCallback(() => {
61
- const isFocused = navigation?.isFocused() ?? true;
62
-
63
- 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) {
64
60
  return null;
65
61
  }
66
62
 
@@ -71,7 +67,7 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
71
67
  }
72
68
 
73
69
  return optionsRef.current;
74
- }, [navigation, getOptionsFromListener]);
70
+ }, [key, getFocusedRouteKey, getOptionsFromListener]);
75
71
 
76
72
  React.useInsertionEffect(() => {
77
73
  // We don't have a parent at the root
@@ -83,15 +79,19 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
83
79
  const addOptionsGetter = React.useCallback(
84
80
  (key: string, getter: () => object | undefined | null) => {
85
81
  optionsGettersFromChildRef.current[key] = getter;
86
- optionsChangeListener();
87
82
 
88
83
  return () => {
89
84
  // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
90
85
  delete optionsGettersFromChildRef.current[key];
91
- 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
+ }
92
92
  };
93
93
  },
94
- [optionsChangeListener]
94
+ [onOptionsChange]
95
95
  );
96
96
 
97
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,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import useLatestCallback from 'use-latest-callback';
2
3
 
3
4
  import { useNavigationBuilderContext } from './NavigationBuilderContext';
4
5
 
@@ -13,8 +14,10 @@ import { useNavigationBuilderContext } from './NavigationBuilderContext';
13
14
  export function useScheduleUpdate(callback: () => void) {
14
15
  const { scheduleUpdate, flushUpdates } = useNavigationBuilderContext();
15
16
 
17
+ const latestCallback = useLatestCallback(callback);
18
+
16
19
  React.useInsertionEffect(() => {
17
- scheduleUpdate(callback);
20
+ scheduleUpdate(latestCallback);
18
21
  });
19
22
 
20
23
  React.useLayoutEffect(flushUpdates);