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