@react-navigation/core 8.0.0-alpha.23 → 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 (153) 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/NavigationBuilderContext.js.map +1 -1
  6. package/lib/module/NavigationIndependentTree.js +7 -6
  7. package/lib/module/NavigationIndependentTree.js.map +1 -1
  8. package/lib/module/NavigationProvider.js +12 -12
  9. package/lib/module/NavigationProvider.js.map +1 -1
  10. package/lib/module/PreventRemoveContext.js.map +1 -1
  11. package/lib/module/PreventRemoveProvider.js +34 -24
  12. package/lib/module/PreventRemoveProvider.js.map +1 -1
  13. package/lib/module/StaticNavigation.js +15 -7
  14. package/lib/module/StaticNavigation.js.map +1 -1
  15. package/lib/module/StaticTreeContext.js +5 -0
  16. package/lib/module/StaticTreeContext.js.map +1 -0
  17. package/lib/module/checkSerializable.js +7 -9
  18. package/lib/module/checkSerializable.js.map +1 -1
  19. package/lib/module/createNavigationContainerRef.js +6 -8
  20. package/lib/module/createNavigationContainerRef.js.map +1 -1
  21. package/lib/module/createNavigatorFactory.js.map +1 -1
  22. package/lib/module/getActionFromState.js +11 -11
  23. package/lib/module/getActionFromState.js.map +1 -1
  24. package/lib/module/getPathFromState.js +125 -96
  25. package/lib/module/getPathFromState.js.map +1 -1
  26. package/lib/module/getPatternParts.js +2 -2
  27. package/lib/module/getPatternParts.js.map +1 -1
  28. package/lib/module/getStateFromPath.js +136 -78
  29. package/lib/module/getStateFromPath.js.map +1 -1
  30. package/lib/module/index.js +1 -0
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/types.js +13 -0
  33. package/lib/module/types.js.map +1 -1
  34. package/lib/module/useEventEmitter.js +5 -2
  35. package/lib/module/useEventEmitter.js.map +1 -1
  36. package/lib/module/useKeyedChildListeners.js +4 -2
  37. package/lib/module/useKeyedChildListeners.js.map +1 -1
  38. package/lib/module/useNavigation.js +2 -15
  39. package/lib/module/useNavigation.js.map +1 -1
  40. package/lib/module/useNavigationBuilder.js +5 -21
  41. package/lib/module/useNavigationBuilder.js.map +1 -1
  42. package/lib/module/useNavigationCache.js +19 -14
  43. package/lib/module/useNavigationCache.js.map +1 -1
  44. package/lib/module/useNavigationHelpers.js +27 -9
  45. package/lib/module/useNavigationHelpers.js.map +1 -1
  46. package/lib/module/useNavigationState.js +69 -36
  47. package/lib/module/useNavigationState.js.map +1 -1
  48. package/lib/module/useOnAction.js +25 -3
  49. package/lib/module/useOnAction.js.map +1 -1
  50. package/lib/module/useOnPreventRemove.js +35 -24
  51. package/lib/module/useOnPreventRemove.js.map +1 -1
  52. package/lib/module/useOptionsGetters.js +3 -1
  53. package/lib/module/useOptionsGetters.js.map +1 -1
  54. package/lib/module/usePreventRemove.js +9 -2
  55. package/lib/module/usePreventRemove.js.map +1 -1
  56. package/lib/module/useRoute.js.map +1 -1
  57. package/lib/module/useRouteCache.js +14 -7
  58. package/lib/module/useRouteCache.js.map +1 -1
  59. package/lib/module/useScheduleUpdate.js +8 -8
  60. package/lib/module/useScheduleUpdate.js.map +1 -1
  61. package/lib/module/useSyncState.js +4 -1
  62. package/lib/module/useSyncState.js.map +1 -1
  63. package/lib/module/validatePathConfig.js +50 -37
  64. package/lib/module/validatePathConfig.js.map +1 -1
  65. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  66. package/lib/typescript/src/DataLoading.d.ts +20 -0
  67. package/lib/typescript/src/DataLoading.d.ts.map +1 -0
  68. package/lib/typescript/src/NavigationBuilderContext.d.ts +2 -2
  69. package/lib/typescript/src/NavigationBuilderContext.d.ts.map +1 -1
  70. package/lib/typescript/src/NavigationProvider.d.ts +0 -1
  71. package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
  72. package/lib/typescript/src/PreventRemoveContext.d.ts +1 -0
  73. package/lib/typescript/src/PreventRemoveContext.d.ts.map +1 -1
  74. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  75. package/lib/typescript/src/StaticNavigation.d.ts +68 -42
  76. package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
  77. package/lib/typescript/src/StaticTreeContext.d.ts +4 -0
  78. package/lib/typescript/src/StaticTreeContext.d.ts.map +1 -0
  79. package/lib/typescript/src/checkSerializable.d.ts +4 -2
  80. package/lib/typescript/src/checkSerializable.d.ts.map +1 -1
  81. package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
  82. package/lib/typescript/src/createNavigatorFactory.d.ts +11 -5
  83. package/lib/typescript/src/createNavigatorFactory.d.ts.map +1 -1
  84. package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
  85. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  86. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  87. package/lib/typescript/src/index.d.ts +2 -1
  88. package/lib/typescript/src/index.d.ts.map +1 -1
  89. package/lib/typescript/src/types.d.ts +84 -29
  90. package/lib/typescript/src/types.d.ts.map +1 -1
  91. package/lib/typescript/src/useEventEmitter.d.ts +1 -1
  92. package/lib/typescript/src/useEventEmitter.d.ts.map +1 -1
  93. package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
  94. package/lib/typescript/src/useNavigation.d.ts.map +1 -1
  95. package/lib/typescript/src/useNavigationBuilder.d.ts +1 -1
  96. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  97. package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
  98. package/lib/typescript/src/useNavigationHelpers.d.ts +3 -4
  99. package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
  100. package/lib/typescript/src/useNavigationState.d.ts +11 -1
  101. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  102. package/lib/typescript/src/useOnAction.d.ts.map +1 -1
  103. package/lib/typescript/src/useOnPreventRemove.d.ts +2 -6
  104. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  105. package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
  106. package/lib/typescript/src/usePreventRemove.d.ts.map +1 -1
  107. package/lib/typescript/src/usePreventRemoveContext.d.ts +1 -0
  108. package/lib/typescript/src/usePreventRemoveContext.d.ts.map +1 -1
  109. package/lib/typescript/src/useRoute.d.ts +1 -1
  110. package/lib/typescript/src/useRoute.d.ts.map +1 -1
  111. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  112. package/lib/typescript/src/useScheduleUpdate.d.ts +5 -4
  113. package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
  114. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  115. package/lib/typescript/src/utilities.d.ts +21 -38
  116. package/lib/typescript/src/utilities.d.ts.map +1 -1
  117. package/lib/typescript/src/validatePathConfig.d.ts.map +1 -1
  118. package/package.json +6 -8
  119. package/src/BaseNavigationContainer.tsx +9 -2
  120. package/src/DataLoading.tsx +139 -0
  121. package/src/NavigationBuilderContext.tsx +5 -1
  122. package/src/NavigationIndependentTree.tsx +11 -11
  123. package/src/NavigationProvider.tsx +13 -16
  124. package/src/PreventRemoveContext.tsx +1 -0
  125. package/src/PreventRemoveProvider.tsx +48 -43
  126. package/src/StaticNavigation.tsx +164 -99
  127. package/src/StaticTreeContext.tsx +7 -0
  128. package/src/checkSerializable.tsx +20 -14
  129. package/src/createNavigationContainerRef.tsx +12 -9
  130. package/src/createNavigatorFactory.tsx +27 -18
  131. package/src/getActionFromState.tsx +18 -16
  132. package/src/getPathFromState.tsx +167 -132
  133. package/src/getPatternParts.tsx +2 -2
  134. package/src/getStateFromPath.tsx +260 -193
  135. package/src/index.tsx +3 -0
  136. package/src/types.tsx +298 -62
  137. package/src/useEventEmitter.tsx +7 -2
  138. package/src/useKeyedChildListeners.tsx +4 -2
  139. package/src/useNavigation.tsx +2 -21
  140. package/src/useNavigationBuilder.tsx +7 -24
  141. package/src/useNavigationCache.tsx +26 -19
  142. package/src/useNavigationHelpers.tsx +26 -9
  143. package/src/useNavigationState.tsx +90 -60
  144. package/src/useOnAction.tsx +55 -3
  145. package/src/useOnPreventRemove.tsx +68 -36
  146. package/src/useOptionsGetters.tsx +4 -1
  147. package/src/usePreventRemove.tsx +11 -2
  148. package/src/useRoute.tsx +3 -5
  149. package/src/useRouteCache.tsx +19 -11
  150. package/src/useScheduleUpdate.tsx +8 -7
  151. package/src/useSyncState.tsx +5 -1
  152. package/src/utilities.tsx +36 -41
  153. package/src/validatePathConfig.tsx +86 -58
@@ -583,7 +583,7 @@ export function useNavigationBuilder<
583
583
 
584
584
  const previousRouteKeyListRef = React.useRef(routeKeyList);
585
585
 
586
- React.useEffect(() => {
586
+ React.useInsertionEffect(() => {
587
587
  previousRouteKeyListRef.current = routeKeyList;
588
588
  });
589
589
 
@@ -789,19 +789,6 @@ export function useNavigationBuilder<
789
789
  // eslint-disable-next-line react-hooks/exhaustive-deps
790
790
  }, []);
791
791
 
792
- // In some cases (e.g. route names change), internal state might have changed
793
- // But it hasn't been committed yet, so hasn't propagated to the sync external store
794
- // During this time, we need to return the internal state in `getState`
795
- // Otherwise it can result in inconsistent state during render in children
796
- // To avoid this, we use a ref for render phase, and immediately clear it on commit
797
- const stateRef = React.useRef<State | null>(state);
798
-
799
- stateRef.current = state;
800
-
801
- React.useLayoutEffect(() => {
802
- stateRef.current = null;
803
- });
804
-
805
792
  const getState = useLatestCallback((): State => {
806
793
  const currentState = getCurrentState();
807
794
 
@@ -877,14 +864,7 @@ export function useNavigationBuilder<
877
864
 
878
865
  listeners.forEach((listener) => listener?.(e));
879
866
  }
880
-
881
- onEmitEvent({
882
- type: e.type,
883
- data: e.data,
884
- target: e.target,
885
- defaultPrevented: e.defaultPrevented,
886
- });
887
- });
867
+ }, onEmitEvent);
888
868
 
889
869
  useFocusEvents({ state, emitter });
890
870
 
@@ -963,9 +943,9 @@ export function useNavigationBuilder<
963
943
  onAction,
964
944
  onUnhandledAction,
965
945
  getState,
946
+ state,
966
947
  emitter,
967
948
  router,
968
- stateRef,
969
949
  });
970
950
 
971
951
  useFocusedListenersChildrenAdapter({
@@ -1020,7 +1000,10 @@ export function useNavigationBuilder<
1020
1000
  return (
1021
1001
  <NavigationMetaContext.Provider value={undefined}>
1022
1002
  <NavigationHelpersContext.Provider value={navigation}>
1023
- <NavigationStateListenerProvider state={state}>
1003
+ <NavigationStateListenerProvider
1004
+ state={state}
1005
+ getState={navigation.getState}
1006
+ >
1024
1007
  <FocusedRouteKeyContext.Provider value={focusedRoute.key}>
1025
1008
  <PreventRemoveProvider>{element}</PreventRemoveProvider>
1026
1009
  </FocusedRouteKeyContext.Provider>
@@ -91,27 +91,15 @@ export function useNavigationCache<
91
91
  [getState, parentNavigationHelpers, navigation, setOptions, emitter]
92
92
  );
93
93
 
94
- cache.current = routes.reduce<
94
+ const next = routes.reduce<
95
95
  NavigationCache<State, ScreenOptions, EventMap, ActionHelpers>
96
96
  >((acc, route) => {
97
97
  const previous = cache.current[route.key];
98
98
 
99
- type Thunk =
100
- | NavigationAction
101
- | ((state: State) => NavigationAction | null | undefined);
102
-
103
99
  if (previous) {
104
100
  // If a cached navigation object already exists, reuse it
105
101
  acc[route.key] = previous;
106
102
  } else {
107
- const dispatch = (thunk: Thunk) => {
108
- const action = typeof thunk === 'function' ? thunk(getState()) : thunk;
109
-
110
- if (action != null) {
111
- navigation.dispatch({ source: route.key, ...action });
112
- }
113
- };
114
-
115
103
  const withStack = (callback: () => void) => {
116
104
  let isStackSet = false;
117
105
 
@@ -134,6 +122,23 @@ export function useNavigationCache<
134
122
  }
135
123
  };
136
124
 
125
+ const dispatch = (
126
+ thunk:
127
+ | NavigationAction
128
+ | ((state: State) => NavigationAction | null | undefined)
129
+ ) => {
130
+ withStack(() =>
131
+ React.startTransition(() => {
132
+ const action =
133
+ typeof thunk === 'function' ? thunk(getState()) : thunk;
134
+
135
+ if (action != null) {
136
+ navigation.dispatch({ source: route.key, ...action });
137
+ }
138
+ })
139
+ );
140
+ };
141
+
137
142
  const actions = {
138
143
  ...router.actionCreators,
139
144
  ...CommonActions,
@@ -142,10 +147,8 @@ export function useNavigationCache<
142
147
  const helpers = Object.keys(actions).reduce<Record<string, () => void>>(
143
148
  (acc, name) => {
144
149
  acc[name] = (...args: any) =>
145
- withStack(() =>
146
- // @ts-expect-error: name is a valid key, but TypeScript is dumb
147
- dispatch(actions[name](...args))
148
- );
150
+ // @ts-expect-error: name is a valid key, but TypeScript is dumb
151
+ dispatch(actions[name](...args));
149
152
 
150
153
  return acc;
151
154
  },
@@ -160,7 +163,7 @@ export function useNavigationCache<
160
163
  ...helpers,
161
164
  // FIXME: too much work to fix the types for now
162
165
  ...(emitter.create(route.key) as any),
163
- dispatch: (thunk: Thunk) => withStack(() => dispatch(thunk)),
166
+ dispatch,
164
167
  getParent: (routeName) => {
165
168
  if (routeName === route.name) {
166
169
  // If the passed route name is the same as the current route's name,
@@ -197,5 +200,9 @@ export function useNavigationCache<
197
200
  return acc;
198
201
  }, {});
199
202
 
200
- return cache.current;
203
+ React.useInsertionEffect(() => {
204
+ cache.current = next;
205
+ });
206
+
207
+ return next;
201
208
  }
@@ -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
 
@@ -88,8 +129,6 @@ export function useOnAction<State extends NavigationState>({
88
129
  result === null && action.target === state.key ? state : result;
89
130
 
90
131
  if (result !== null) {
91
- onDispatchAction(action, state === result);
92
-
93
132
  if (state !== result) {
94
133
  const isPrevented = shouldPreventRemove(
95
134
  emitter,
@@ -100,10 +139,22 @@ export function useOnAction<State extends NavigationState>({
100
139
  );
101
140
 
102
141
  if (isPrevented) {
142
+ onDispatchAction(action, true);
143
+
103
144
  return true;
104
145
  }
105
146
 
147
+ onDispatchAction(action, false);
148
+
149
+ if (tree && didFocusedRouteChange(state, result)) {
150
+ const loader = getLoaderForState(tree, result);
151
+
152
+ loader?.();
153
+ }
154
+
106
155
  setState(result);
156
+ } else {
157
+ onDispatchAction(action, true);
107
158
  }
108
159
 
109
160
  if (onRouteFocusParent !== undefined) {
@@ -151,6 +202,7 @@ export function useOnAction<State extends NavigationState>({
151
202
  onRouteFocusParent,
152
203
  router,
153
204
  setState,
205
+ tree,
154
206
  ]
155
207
  );
156
208
 
@@ -1,6 +1,9 @@
1
1
  import type {
2
2
  NavigationAction,
3
+ NavigationRoute,
3
4
  NavigationState,
5
+ ParamListBase,
6
+ PartialRoute,
4
7
  } from '@react-navigation/routers';
5
8
  import * as React from 'react';
6
9
 
@@ -23,16 +26,18 @@ const VISITED_ROUTE_KEYS = Symbol('VISITED_ROUTE_KEYS');
23
26
  export const shouldPreventRemove = (
24
27
  emitter: NavigationEventEmitter<EventMapCore<any>>,
25
28
  beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>,
26
- currentRoutes: { key: string }[],
27
- nextRoutes: { key?: string | undefined }[],
29
+ currentRoutes: NavigationRoute<ParamListBase, string>[],
30
+ nextRoutes: (
31
+ | NavigationRoute<ParamListBase, string>
32
+ | PartialRoute<NavigationRoute<ParamListBase, string>>
33
+ )[],
28
34
  action: NavigationAction
29
35
  ) => {
30
- const nextRouteKeys = nextRoutes.map((route) => route.key);
31
-
32
- // Call these in reverse order so last screens handle the event first
33
- const removedRoutes = currentRoutes
34
- .filter((route) => !nextRouteKeys.includes(route.key))
35
- .reverse();
36
+ const nextRoutesByKey = new Map(
37
+ nextRoutes
38
+ .map((route) => [route.key, route] as const)
39
+ .filter(([, route]) => route.key != null)
40
+ );
36
41
 
37
42
  const visitedRouteKeys: Set<string> =
38
43
  // @ts-expect-error: add this property to mark that we've already emitted this action
@@ -43,30 +48,53 @@ export const shouldPreventRemove = (
43
48
  [VISITED_ROUTE_KEYS]: visitedRouteKeys,
44
49
  };
45
50
 
46
- for (const route of removedRoutes) {
51
+ // Call these in reverse order so last screens handle the event first
52
+ const reversedCurrentRoutes = [...currentRoutes].reverse();
53
+
54
+ for (const route of reversedCurrentRoutes) {
47
55
  if (visitedRouteKeys.has(route.key)) {
48
56
  // Skip if we've already emitted this action for this screen
49
57
  continue;
50
58
  }
51
59
 
52
- // First, we need to check if any child screens want to prevent it
53
- const isPrevented = beforeRemoveListeners[route.key]?.(beforeRemoveAction);
54
-
55
- if (isPrevented) {
56
- return true;
57
- }
58
-
59
- visitedRouteKeys.add(route.key);
60
+ if (!nextRoutesByKey.has(route.key)) {
61
+ // The route is not in next state, so it's being removed
62
+ // First, we need to check if any child screens want to prevent it
63
+ const isPrevented = beforeRemoveListeners[route.key]?.(
64
+ beforeRemoveAction,
65
+ undefined
66
+ );
67
+
68
+ if (isPrevented) {
69
+ return true;
70
+ }
71
+
72
+ visitedRouteKeys.add(route.key);
73
+
74
+ const event = emitter.emit({
75
+ type: 'beforeRemove',
76
+ target: route.key,
77
+ data: { action: beforeRemoveAction },
78
+ canPreventDefault: true,
79
+ });
60
80
 
61
- const event = emitter.emit({
62
- type: 'beforeRemove',
63
- target: route.key,
64
- data: { action: beforeRemoveAction },
65
- canPreventDefault: true,
66
- });
81
+ if (event.defaultPrevented) {
82
+ return true;
83
+ }
84
+ } else {
85
+ // The route is kept but its nested state changed, so propagate the check into the nested navigator
86
+ const nextRoute = nextRoutesByKey.get(route.key);
87
+
88
+ if (route.state != null && route.state !== nextRoute?.state) {
89
+ const isPrevented = beforeRemoveListeners[route.key]?.(
90
+ beforeRemoveAction,
91
+ nextRoute?.state
92
+ );
67
93
 
68
- if (event.defaultPrevented) {
69
- return true;
94
+ if (isPrevented) {
95
+ return true;
96
+ }
97
+ }
70
98
  }
71
99
  }
72
100
 
@@ -84,17 +112,21 @@ export function useOnPreventRemove({
84
112
 
85
113
  React.useInsertionEffect(() => {
86
114
  if (routeKey) {
87
- return addKeyedListener?.('beforeRemove', routeKey, (action) => {
88
- const state = getState();
89
-
90
- return shouldPreventRemove(
91
- emitter,
92
- beforeRemoveListeners,
93
- state.routes,
94
- [],
95
- action
96
- );
97
- });
115
+ return addKeyedListener?.(
116
+ 'beforeRemove',
117
+ routeKey,
118
+ (action, nextState) => {
119
+ const state = getState();
120
+
121
+ return shouldPreventRemove(
122
+ emitter,
123
+ beforeRemoveListeners,
124
+ state.routes,
125
+ nextState?.routes ?? [],
126
+ action
127
+ );
128
+ }
129
+ );
98
130
  }
99
131
  }, [addKeyedListener, beforeRemoveListeners, emitter, getState, routeKey]);
100
132
  }