@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.
- package/lib/module/BaseNavigationContainer.js +5 -2
- package/lib/module/BaseNavigationContainer.js.map +1 -1
- package/lib/module/DataLoading.js +102 -0
- package/lib/module/DataLoading.js.map +1 -0
- package/lib/module/NavigationBuilderContext.js.map +1 -1
- package/lib/module/NavigationIndependentTree.js +7 -6
- package/lib/module/NavigationIndependentTree.js.map +1 -1
- package/lib/module/NavigationProvider.js +12 -12
- package/lib/module/NavigationProvider.js.map +1 -1
- package/lib/module/PreventRemoveContext.js.map +1 -1
- package/lib/module/PreventRemoveProvider.js +34 -24
- package/lib/module/PreventRemoveProvider.js.map +1 -1
- package/lib/module/StaticNavigation.js +15 -7
- package/lib/module/StaticNavigation.js.map +1 -1
- package/lib/module/StaticTreeContext.js +5 -0
- package/lib/module/StaticTreeContext.js.map +1 -0
- package/lib/module/checkSerializable.js +7 -9
- package/lib/module/checkSerializable.js.map +1 -1
- package/lib/module/createNavigationContainerRef.js +6 -8
- package/lib/module/createNavigationContainerRef.js.map +1 -1
- package/lib/module/createNavigatorFactory.js.map +1 -1
- package/lib/module/getActionFromState.js +11 -11
- package/lib/module/getActionFromState.js.map +1 -1
- package/lib/module/getPathFromState.js +125 -96
- package/lib/module/getPathFromState.js.map +1 -1
- package/lib/module/getPatternParts.js +2 -2
- package/lib/module/getPatternParts.js.map +1 -1
- package/lib/module/getStateFromPath.js +136 -78
- package/lib/module/getStateFromPath.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +13 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/useEventEmitter.js +5 -2
- package/lib/module/useEventEmitter.js.map +1 -1
- package/lib/module/useKeyedChildListeners.js +4 -2
- package/lib/module/useKeyedChildListeners.js.map +1 -1
- package/lib/module/useNavigation.js +2 -15
- package/lib/module/useNavigation.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +5 -21
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useNavigationCache.js +19 -14
- package/lib/module/useNavigationCache.js.map +1 -1
- package/lib/module/useNavigationHelpers.js +27 -9
- package/lib/module/useNavigationHelpers.js.map +1 -1
- package/lib/module/useNavigationState.js +69 -36
- package/lib/module/useNavigationState.js.map +1 -1
- package/lib/module/useOnAction.js +25 -3
- package/lib/module/useOnAction.js.map +1 -1
- package/lib/module/useOnPreventRemove.js +35 -24
- package/lib/module/useOnPreventRemove.js.map +1 -1
- package/lib/module/useOptionsGetters.js +3 -1
- package/lib/module/useOptionsGetters.js.map +1 -1
- package/lib/module/usePreventRemove.js +9 -2
- package/lib/module/usePreventRemove.js.map +1 -1
- package/lib/module/useRoute.js.map +1 -1
- package/lib/module/useRouteCache.js +14 -7
- package/lib/module/useRouteCache.js.map +1 -1
- package/lib/module/useScheduleUpdate.js +8 -8
- package/lib/module/useScheduleUpdate.js.map +1 -1
- package/lib/module/useSyncState.js +4 -1
- package/lib/module/useSyncState.js.map +1 -1
- package/lib/module/validatePathConfig.js +50 -37
- package/lib/module/validatePathConfig.js.map +1 -1
- package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
- package/lib/typescript/src/DataLoading.d.ts +20 -0
- package/lib/typescript/src/DataLoading.d.ts.map +1 -0
- package/lib/typescript/src/NavigationBuilderContext.d.ts +2 -2
- package/lib/typescript/src/NavigationBuilderContext.d.ts.map +1 -1
- package/lib/typescript/src/NavigationProvider.d.ts +0 -1
- package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
- package/lib/typescript/src/PreventRemoveContext.d.ts +1 -0
- package/lib/typescript/src/PreventRemoveContext.d.ts.map +1 -1
- package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
- package/lib/typescript/src/StaticNavigation.d.ts +68 -42
- package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
- package/lib/typescript/src/StaticTreeContext.d.ts +4 -0
- package/lib/typescript/src/StaticTreeContext.d.ts.map +1 -0
- package/lib/typescript/src/checkSerializable.d.ts +4 -2
- package/lib/typescript/src/checkSerializable.d.ts.map +1 -1
- package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
- package/lib/typescript/src/createNavigatorFactory.d.ts +11 -5
- package/lib/typescript/src/createNavigatorFactory.d.ts.map +1 -1
- package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
- package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +84 -29
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useEventEmitter.d.ts +1 -1
- package/lib/typescript/src/useEventEmitter.d.ts.map +1 -1
- package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
- package/lib/typescript/src/useNavigation.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationHelpers.d.ts +3 -4
- package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationState.d.ts +11 -1
- package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
- package/lib/typescript/src/useOnAction.d.ts.map +1 -1
- package/lib/typescript/src/useOnPreventRemove.d.ts +2 -6
- package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
- package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
- package/lib/typescript/src/usePreventRemove.d.ts.map +1 -1
- package/lib/typescript/src/usePreventRemoveContext.d.ts +1 -0
- package/lib/typescript/src/usePreventRemoveContext.d.ts.map +1 -1
- package/lib/typescript/src/useRoute.d.ts +1 -1
- package/lib/typescript/src/useRoute.d.ts.map +1 -1
- package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
- package/lib/typescript/src/useScheduleUpdate.d.ts +5 -4
- package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
- package/lib/typescript/src/useSyncState.d.ts.map +1 -1
- package/lib/typescript/src/utilities.d.ts +21 -38
- package/lib/typescript/src/utilities.d.ts.map +1 -1
- package/lib/typescript/src/validatePathConfig.d.ts.map +1 -1
- package/package.json +6 -8
- package/src/BaseNavigationContainer.tsx +9 -2
- package/src/DataLoading.tsx +139 -0
- package/src/NavigationBuilderContext.tsx +5 -1
- package/src/NavigationIndependentTree.tsx +11 -11
- package/src/NavigationProvider.tsx +13 -16
- package/src/PreventRemoveContext.tsx +1 -0
- package/src/PreventRemoveProvider.tsx +48 -43
- package/src/StaticNavigation.tsx +164 -99
- package/src/StaticTreeContext.tsx +7 -0
- package/src/checkSerializable.tsx +20 -14
- package/src/createNavigationContainerRef.tsx +12 -9
- package/src/createNavigatorFactory.tsx +27 -18
- package/src/getActionFromState.tsx +18 -16
- package/src/getPathFromState.tsx +167 -132
- package/src/getPatternParts.tsx +2 -2
- package/src/getStateFromPath.tsx +260 -193
- package/src/index.tsx +3 -0
- package/src/types.tsx +298 -62
- package/src/useEventEmitter.tsx +7 -2
- package/src/useKeyedChildListeners.tsx +4 -2
- package/src/useNavigation.tsx +2 -21
- package/src/useNavigationBuilder.tsx +7 -24
- package/src/useNavigationCache.tsx +26 -19
- package/src/useNavigationHelpers.tsx +26 -9
- package/src/useNavigationState.tsx +90 -60
- package/src/useOnAction.tsx +55 -3
- package/src/useOnPreventRemove.tsx +68 -36
- package/src/useOptionsGetters.tsx +4 -1
- package/src/usePreventRemove.tsx +11 -2
- package/src/useRoute.tsx +3 -5
- package/src/useRouteCache.tsx +19 -11
- package/src/useScheduleUpdate.tsx +8 -7
- package/src/useSyncState.tsx +5 -1
- package/src/utilities.tsx +36 -41
- 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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
-
|
|
51
|
-
stateListener: NavigationStateListener | undefined,
|
|
52
|
-
selector;
|
|
42
|
+
const selector = typeof args[0] === 'string' ? args[1] : args[0];
|
|
53
43
|
|
|
54
|
-
if (typeof
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
90
|
-
? navigation.getState
|
|
91
|
-
: stateListener?.getState;
|
|
79
|
+
const [, forceUpdate] = React.useReducer((count: number) => count + 1, 0);
|
|
92
80
|
|
|
93
|
-
|
|
94
|
-
throw new Error("Couldn't get the navigation state. This is not expected.");
|
|
95
|
-
}
|
|
81
|
+
const selected = select(getState());
|
|
96
82
|
|
|
97
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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);
|
package/src/useOnAction.tsx
CHANGED
|
@@ -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.
|
|
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:
|
|
27
|
-
nextRoutes:
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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?.(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
}
|