@react-navigation/core 8.0.0-alpha.24 → 8.0.0-alpha.26
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/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 +14 -6
- 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 +38 -39
- 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 +72 -45
- 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.map +1 -1
- package/lib/module/useEventEmitter.js +1 -1
- 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 +4 -14
- 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 +21 -2
- package/lib/module/useOnAction.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/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.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/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 +65 -39
- 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 +9 -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 +27 -18
- package/lib/typescript/src/types.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/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/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 +17 -0
- package/lib/typescript/src/utilities.d.ts.map +1 -1
- package/lib/typescript/src/validatePathConfig.d.ts.map +1 -1
- package/package.json +3 -5
- package/src/BaseNavigationContainer.tsx +9 -2
- package/src/DataLoading.tsx +139 -0
- 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 +157 -93
- package/src/StaticTreeContext.tsx +7 -0
- package/src/checkSerializable.tsx +20 -14
- package/src/createNavigationContainerRef.tsx +12 -9
- package/src/createNavigatorFactory.tsx +16 -28
- package/src/getActionFromState.tsx +18 -16
- package/src/getPathFromState.tsx +69 -65
- package/src/getPatternParts.tsx +2 -2
- package/src/getStateFromPath.tsx +107 -66
- package/src/index.tsx +2 -0
- package/src/types.tsx +74 -55
- package/src/useEventEmitter.tsx +1 -1
- package/src/useKeyedChildListeners.tsx +4 -2
- package/src/useNavigation.tsx +2 -21
- package/src/useNavigationBuilder.tsx +6 -16
- package/src/useNavigationCache.tsx +26 -19
- package/src/useNavigationHelpers.tsx +26 -9
- package/src/useNavigationState.tsx +90 -60
- package/src/useOnAction.tsx +49 -1
- package/src/useOptionsGetters.tsx +4 -1
- package/src/usePreventRemove.tsx +11 -2
- package/src/useRouteCache.tsx +19 -11
- package/src/useScheduleUpdate.tsx +8 -7
- package/src/useSyncState.tsx +5 -1
- package/src/utilities.tsx +28 -0
- package/src/validatePathConfig.tsx +1 -0
|
@@ -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
|
|
|
@@ -105,6 +146,12 @@ export function useOnAction<State extends NavigationState>({
|
|
|
105
146
|
|
|
106
147
|
onDispatchAction(action, false);
|
|
107
148
|
|
|
149
|
+
if (tree && didFocusedRouteChange(state, result)) {
|
|
150
|
+
const loader = getLoaderForState(tree, result);
|
|
151
|
+
|
|
152
|
+
loader?.();
|
|
153
|
+
}
|
|
154
|
+
|
|
108
155
|
setState(result);
|
|
109
156
|
} else {
|
|
110
157
|
onDispatchAction(action, true);
|
|
@@ -155,6 +202,7 @@ export function useOnAction<State extends NavigationState>({
|
|
|
155
202
|
onRouteFocusParent,
|
|
156
203
|
router,
|
|
157
204
|
setState,
|
|
205
|
+
tree,
|
|
158
206
|
]
|
|
159
207
|
);
|
|
160
208
|
|
|
@@ -31,8 +31,11 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
|
|
|
31
31
|
}
|
|
32
32
|
}, [navigation, onOptionsChange]);
|
|
33
33
|
|
|
34
|
-
React.
|
|
34
|
+
React.useInsertionEffect(() => {
|
|
35
35
|
optionsRef.current = options;
|
|
36
|
+
}, [options]);
|
|
37
|
+
|
|
38
|
+
React.useEffect(() => {
|
|
36
39
|
optionsChangeListener();
|
|
37
40
|
|
|
38
41
|
return navigation?.addListener('focus', optionsChangeListener);
|
package/src/usePreventRemove.tsx
CHANGED
|
@@ -23,15 +23,24 @@ export function usePreventRemove(
|
|
|
23
23
|
const navigation = useNavigation();
|
|
24
24
|
const { key: routeKey } = useRoute();
|
|
25
25
|
|
|
26
|
-
const { setPreventRemove } = usePreventRemoveContext();
|
|
26
|
+
const { setPreventRemove, notifyPreventRemove } = usePreventRemoveContext();
|
|
27
27
|
|
|
28
|
-
React.
|
|
28
|
+
React.useInsertionEffect(() => {
|
|
29
29
|
setPreventRemove(id, routeKey, preventRemove);
|
|
30
|
+
|
|
30
31
|
return () => {
|
|
31
32
|
setPreventRemove(id, routeKey, false);
|
|
32
33
|
};
|
|
33
34
|
}, [setPreventRemove, id, routeKey, preventRemove]);
|
|
34
35
|
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
notifyPreventRemove();
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
notifyPreventRemove();
|
|
41
|
+
};
|
|
42
|
+
}, [id, routeKey, preventRemove, notifyPreventRemove]);
|
|
43
|
+
|
|
35
44
|
const beforeRemoveListener = useLatestCallback<
|
|
36
45
|
EventListenerCallback<EventMapCore<any>, 'beforeRemove'>
|
|
37
46
|
>((e) => {
|
package/src/useRouteCache.tsx
CHANGED
|
@@ -4,8 +4,6 @@ import * as React from 'react';
|
|
|
4
4
|
import { isRecordEqual } from './isRecordEqual';
|
|
5
5
|
import type { RouteProp } from './types';
|
|
6
6
|
|
|
7
|
-
type RouteCache = Map<string, RouteProp<ParamListBase>>;
|
|
8
|
-
|
|
9
7
|
/**
|
|
10
8
|
* Utilities such as `getFocusedRouteNameFromRoute` need to access state.
|
|
11
9
|
* So we need a way to suppress the warning for those use cases.
|
|
@@ -21,9 +19,12 @@ export function useRouteCache<State extends NavigationState>(
|
|
|
21
19
|
routes: State['routes']
|
|
22
20
|
) {
|
|
23
21
|
// Cache object which holds route objects for each screen
|
|
24
|
-
const cache = React.useMemo(
|
|
22
|
+
const cache = React.useMemo(
|
|
23
|
+
() => ({ current: new Map<string, RouteProp<ParamListBase>>() }),
|
|
24
|
+
[]
|
|
25
|
+
);
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
const next = routes.reduce((acc, route) => {
|
|
27
28
|
const previous = cache.current.get(route.key);
|
|
28
29
|
const { state, ...routeWithoutState } = route;
|
|
29
30
|
|
|
@@ -53,16 +54,23 @@ export function useRouteCache<State extends NavigationState>(
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
// @ts-expect-error: this isn't in type definitions coz we want this private
|
|
58
|
+
if (proxy[CHILD_STATE] !== state) {
|
|
59
|
+
Object.defineProperty(proxy, CHILD_STATE, {
|
|
60
|
+
enumerable: false,
|
|
61
|
+
configurable: true,
|
|
62
|
+
value: state,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
61
65
|
|
|
62
66
|
acc.set(route.key, proxy);
|
|
63
67
|
|
|
64
68
|
return acc;
|
|
65
|
-
}, new Map()
|
|
69
|
+
}, new Map<string, RouteProp<ParamListBase>>());
|
|
70
|
+
|
|
71
|
+
React.useInsertionEffect(() => {
|
|
72
|
+
cache.current = next;
|
|
73
|
+
});
|
|
66
74
|
|
|
67
|
-
return Array.from(
|
|
75
|
+
return Array.from(next.values());
|
|
68
76
|
}
|
|
@@ -4,17 +4,18 @@ import { NavigationBuilderContext } from './NavigationBuilderContext';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* When screen config changes, we want to update the navigator in the same update phase.
|
|
7
|
-
* However, navigation state is in the root component and React won't let us update it from a child.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* However, navigation state is in the root component and React won't let us update it from a child during render.
|
|
8
|
+
* The render could also be interrupted in concurrent mode, so we need to update after commit.
|
|
9
|
+
* So we queue the update in an insertion effect and flush it in a layout effect.
|
|
10
|
+
* Insertion effects run before layout effects, so every navigator in the tree has queued its update before we flush.
|
|
11
|
+
* This lets nested updates be applied from the root down without clobbering each other.
|
|
11
12
|
*/
|
|
12
13
|
export function useScheduleUpdate(callback: () => void) {
|
|
13
14
|
const { scheduleUpdate, flushUpdates } = React.use(NavigationBuilderContext);
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
React.useInsertionEffect(() => {
|
|
17
|
+
scheduleUpdate(callback);
|
|
18
|
+
});
|
|
18
19
|
|
|
19
20
|
React.useLayoutEffect(flushUpdates);
|
|
20
21
|
}
|
package/src/useSyncState.tsx
CHANGED
|
@@ -100,7 +100,11 @@ export function useSyncState<T>(getInitialState: () => T) {
|
|
|
100
100
|
|
|
101
101
|
if (pendingUpdates.length !== 0) {
|
|
102
102
|
store.batchUpdates(() => {
|
|
103
|
-
//
|
|
103
|
+
// Updates are queued child-first as insertion effects run bottom-up.
|
|
104
|
+
// The root navigator replaces the whole state while nested navigators merge into it.
|
|
105
|
+
// So we apply them root-first to avoid the root clobbering nested updates.
|
|
106
|
+
pendingUpdates.reverse();
|
|
107
|
+
|
|
104
108
|
for (const update of pendingUpdates) {
|
|
105
109
|
update();
|
|
106
110
|
}
|
package/src/utilities.tsx
CHANGED
|
@@ -15,6 +15,17 @@ export type KeysOf<T> = T extends {} ? keyof T : never;
|
|
|
15
15
|
*/
|
|
16
16
|
export type KeyOf<T extends {}> = Extract<keyof T, string>;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* TS only checks object literals for excess properties when they are not generic.
|
|
20
|
+
* When an object is captured as a generic type, unknown keys are not caught.
|
|
21
|
+
* This builds a type from the provided object where any key not in the allowed type has the type `never`.
|
|
22
|
+
* Checking the provided object against this type then produces an error for each unknown key.
|
|
23
|
+
*/
|
|
24
|
+
export type NoExcessObject<Provided, Allowed> = Record<
|
|
25
|
+
Exclude<keyof Provided, keyof Allowed>,
|
|
26
|
+
never
|
|
27
|
+
>;
|
|
28
|
+
|
|
18
29
|
/**
|
|
19
30
|
* We get a union type when using keyof, but we want an intersection instead.
|
|
20
31
|
* https://stackoverflow.com/a/50375286/1665026
|
|
@@ -37,6 +48,23 @@ export type RequiredKeys<T extends object> = {
|
|
|
37
48
|
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
|
|
38
49
|
}[keyof T];
|
|
39
50
|
|
|
51
|
+
type OptionalKeys<T extends object> = Exclude<keyof T, RequiredKeys<T>>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Remove `| undefined` from the object type,
|
|
55
|
+
* and adjust the optionality of the keys based on the original type.
|
|
56
|
+
*/
|
|
57
|
+
export type NotUndefinedObject<
|
|
58
|
+
T extends object | undefined,
|
|
59
|
+
O extends object = NotUndefined<T>,
|
|
60
|
+
> = undefined extends T
|
|
61
|
+
? {
|
|
62
|
+
[K in RequiredKeys<O>]: O[K];
|
|
63
|
+
} & {
|
|
64
|
+
[K in OptionalKeys<O>]?: O[K] | undefined;
|
|
65
|
+
}
|
|
66
|
+
: T;
|
|
67
|
+
|
|
40
68
|
export type UndefinedIfAllOptional<T> = T extends object
|
|
41
69
|
? RequiredKeys<T> extends never
|
|
42
70
|
? T | undefined
|