@react-navigation/core 7.21.3 → 7.21.5
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/PreventRemoveContext.js.map +1 -1
- package/lib/module/PreventRemoveProvider.js +34 -24
- package/lib/module/PreventRemoveProvider.js.map +1 -1
- package/lib/module/createNavigationContainerRef.js +6 -8
- package/lib/module/createNavigationContainerRef.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/useNavigationBuilder.js +4 -15
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useNavigationCache.js +5 -2
- 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 +44 -11
- package/lib/module/useNavigationState.js.map +1 -1
- package/lib/module/useOnAction.js +1 -1
- 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/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/createNavigationContainerRef.d.ts.map +1 -1
- package/lib/typescript/src/useKeyedChildListeners.d.ts.map +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 +2 -3
- package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationState.d.ts +2 -1
- package/lib/typescript/src/useNavigationState.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/package.json +2 -2
- package/src/PreventRemoveContext.tsx +1 -0
- package/src/PreventRemoveProvider.tsx +48 -43
- package/src/createNavigationContainerRef.tsx +12 -9
- package/src/useEventEmitter.tsx +1 -1
- package/src/useKeyedChildListeners.tsx +4 -2
- package/src/useNavigationBuilder.tsx +6 -17
- package/src/useNavigationCache.tsx +6 -2
- package/src/useNavigationHelpers.tsx +26 -9
- package/src/useNavigationState.tsx +51 -21
- package/src/useOnAction.tsx +1 -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
|
@@ -13,13 +13,7 @@ type Props = {
|
|
|
13
13
|
children: React.ReactNode;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
type PreventedRoutesMap = Map<
|
|
17
|
-
string,
|
|
18
|
-
{
|
|
19
|
-
routeKey: string;
|
|
20
|
-
preventRemove: boolean;
|
|
21
|
-
}
|
|
22
|
-
>;
|
|
16
|
+
type PreventedRoutesMap = Map<string, string>;
|
|
23
17
|
|
|
24
18
|
/**
|
|
25
19
|
* Util function to transform map of prevented routes to a simpler object.
|
|
@@ -27,17 +21,11 @@ type PreventedRoutesMap = Map<
|
|
|
27
21
|
const transformPreventedRoutes = (
|
|
28
22
|
preventedRoutesMap: PreventedRoutesMap
|
|
29
23
|
): PreventedRoutes => {
|
|
30
|
-
const
|
|
24
|
+
const preventedRoutes: PreventedRoutes = {};
|
|
31
25
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
preventRemove: acc[routeKey]?.preventRemove || preventRemove,
|
|
36
|
-
};
|
|
37
|
-
return acc;
|
|
38
|
-
},
|
|
39
|
-
{}
|
|
40
|
-
);
|
|
26
|
+
for (const routeKey of preventedRoutesMap.values()) {
|
|
27
|
+
preventedRoutes[routeKey] = { preventRemove: true };
|
|
28
|
+
}
|
|
41
29
|
|
|
42
30
|
return preventedRoutes;
|
|
43
31
|
};
|
|
@@ -50,12 +38,15 @@ export function PreventRemoveProvider({ children }: Props) {
|
|
|
50
38
|
const [preventedRoutesMap, setPreventedRoutesMap] =
|
|
51
39
|
React.useState<PreventedRoutesMap>(() => new Map());
|
|
52
40
|
|
|
41
|
+
const registry = React.useRef<PreventedRoutesMap>(new Map());
|
|
42
|
+
|
|
53
43
|
const navigation = React.useContext(NavigationHelpersContext);
|
|
54
44
|
const route = React.useContext(NavigationRouteContext);
|
|
55
45
|
|
|
56
46
|
const preventRemoveContextValue = React.useContext(PreventRemoveContext);
|
|
57
47
|
// take `setPreventRemove` from parent context - if exist it means we're in a nested context
|
|
58
48
|
const setParentPrevented = preventRemoveContextValue?.setPreventRemove;
|
|
49
|
+
const notifyParentPrevented = preventRemoveContextValue?.notifyPreventRemove;
|
|
59
50
|
|
|
60
51
|
const setPreventRemove = useLatestCallback(
|
|
61
52
|
(id: string, routeKey: string, preventRemove: boolean): void => {
|
|
@@ -71,36 +62,49 @@ export function PreventRemoveProvider({ children }: Props) {
|
|
|
71
62
|
);
|
|
72
63
|
}
|
|
73
64
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
) {
|
|
80
|
-
return prevPrevented;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const nextPrevented = new Map(prevPrevented);
|
|
84
|
-
|
|
85
|
-
if (preventRemove) {
|
|
86
|
-
nextPrevented.set(id, {
|
|
87
|
-
routeKey,
|
|
88
|
-
preventRemove,
|
|
89
|
-
});
|
|
90
|
-
} else {
|
|
91
|
-
nextPrevented.delete(id);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return nextPrevented;
|
|
95
|
-
});
|
|
65
|
+
if (preventRemove) {
|
|
66
|
+
registry.current.set(id, routeKey);
|
|
67
|
+
} else {
|
|
68
|
+
registry.current.delete(id);
|
|
69
|
+
}
|
|
96
70
|
}
|
|
97
71
|
);
|
|
98
72
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
73
|
+
const notifyPreventRemove = useLatestCallback(() => {
|
|
74
|
+
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
|
75
|
+
setPreventedRoutesMap((prevPrevented) => {
|
|
76
|
+
const nextPrevented = registry.current;
|
|
102
77
|
|
|
78
|
+
const hasChanged =
|
|
79
|
+
prevPrevented.size !== nextPrevented.size ||
|
|
80
|
+
[...nextPrevented].some(
|
|
81
|
+
([id, routeKey]) => prevPrevented.get(id) !== routeKey
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// values haven't changed - do nothing
|
|
85
|
+
if (!hasChanged) {
|
|
86
|
+
return prevPrevented;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return new Map(nextPrevented);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Sync the state on every commit in case the registry was changed by a screen
|
|
94
|
+
// unmounting while hidden with `<Activity>`, where no passive effects run
|
|
95
|
+
// Also notify the parent so nested prevention propagates upwards
|
|
103
96
|
React.useEffect(() => {
|
|
97
|
+
notifyPreventRemove();
|
|
98
|
+
notifyParentPrevented?.();
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
notifyParentPrevented?.();
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const isPrevented = preventedRoutesMap.size > 0;
|
|
106
|
+
|
|
107
|
+
React.useInsertionEffect(() => {
|
|
104
108
|
if (route?.key !== undefined && setParentPrevented !== undefined) {
|
|
105
109
|
// when route is defined (and setParentPrevented) it means we're in a nested stack
|
|
106
110
|
// route.key then will be the route key of parent
|
|
@@ -116,9 +120,10 @@ export function PreventRemoveProvider({ children }: Props) {
|
|
|
116
120
|
const value = React.useMemo(
|
|
117
121
|
() => ({
|
|
118
122
|
setPreventRemove,
|
|
123
|
+
notifyPreventRemove,
|
|
119
124
|
preventedRoutes: transformPreventedRoutes(preventedRoutesMap),
|
|
120
125
|
}),
|
|
121
|
-
[setPreventRemove, preventedRoutesMap]
|
|
126
|
+
[setPreventRemove, notifyPreventRemove, preventedRoutesMap]
|
|
122
127
|
);
|
|
123
128
|
|
|
124
129
|
return (
|
|
@@ -29,6 +29,8 @@ export function createNavigationContainerRef<
|
|
|
29
29
|
|
|
30
30
|
const listeners: Record<string, ((...args: any[]) => void)[]> = {};
|
|
31
31
|
|
|
32
|
+
let current: NavigationContainerRef<ParamList> | null = null;
|
|
33
|
+
|
|
32
34
|
const removeListener = (
|
|
33
35
|
event: string,
|
|
34
36
|
callback: (...args: any[]) => void
|
|
@@ -36,9 +38,12 @@ export function createNavigationContainerRef<
|
|
|
36
38
|
if (listeners[event]) {
|
|
37
39
|
listeners[event] = listeners[event].filter((cb) => cb !== callback);
|
|
38
40
|
}
|
|
39
|
-
};
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
current?.removeListener(
|
|
43
|
+
event as keyof NavigationContainerEventMap,
|
|
44
|
+
callback
|
|
45
|
+
);
|
|
46
|
+
};
|
|
42
47
|
|
|
43
48
|
const ref: NavigationContainerRefWithCurrent<ParamList> = {
|
|
44
49
|
get current() {
|
|
@@ -67,7 +72,11 @@ export function createNavigationContainerRef<
|
|
|
67
72
|
},
|
|
68
73
|
...methods.reduce<any>((acc, name) => {
|
|
69
74
|
acc[name] = (...args: any[]) => {
|
|
70
|
-
if (
|
|
75
|
+
if (name === 'removeListener') {
|
|
76
|
+
const [event, callback] = args;
|
|
77
|
+
|
|
78
|
+
removeListener(event, callback);
|
|
79
|
+
} else if (current == null) {
|
|
71
80
|
switch (name) {
|
|
72
81
|
case 'addListener': {
|
|
73
82
|
const [event, callback] = args;
|
|
@@ -77,12 +86,6 @@ export function createNavigationContainerRef<
|
|
|
77
86
|
|
|
78
87
|
return () => removeListener(event, callback);
|
|
79
88
|
}
|
|
80
|
-
case 'removeListener': {
|
|
81
|
-
const [event, callback] = args;
|
|
82
|
-
|
|
83
|
-
removeListener(event, callback);
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
89
|
default:
|
|
87
90
|
console.error(NOT_INITIALIZED_ERROR);
|
|
88
91
|
}
|
package/src/useEventEmitter.tsx
CHANGED
|
@@ -19,7 +19,7 @@ export function useEventEmitter<T extends Record<string, any>>(
|
|
|
19
19
|
const listenRef = React.useRef(listen);
|
|
20
20
|
const onPostEmitRef = React.useRef(onPostEmit);
|
|
21
21
|
|
|
22
|
-
React.
|
|
22
|
+
React.useInsertionEffect(() => {
|
|
23
23
|
listenRef.current = listen;
|
|
24
24
|
onPostEmitRef.current = onPostEmit;
|
|
25
25
|
});
|
|
@@ -28,8 +28,10 @@ export function useKeyedChildListeners() {
|
|
|
28
28
|
keyedListeners[type][key] = listener;
|
|
29
29
|
|
|
30
30
|
return () => {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if (keyedListeners[type][key] === listener) {
|
|
32
|
+
// @ts-expect-error: according to ref stated above you can use `key` to index type
|
|
33
|
+
keyedListeners[type][key] = undefined;
|
|
34
|
+
}
|
|
33
35
|
};
|
|
34
36
|
},
|
|
35
37
|
[keyedListeners]
|
|
@@ -37,7 +37,6 @@ import {
|
|
|
37
37
|
} from './types';
|
|
38
38
|
import { UnhandledActionContext } from './UnhandledActionContext';
|
|
39
39
|
import { useChildListeners } from './useChildListeners';
|
|
40
|
-
import { useClientLayoutEffect } from './useClientLayoutEffect';
|
|
41
40
|
import { useComponent } from './useComponent';
|
|
42
41
|
import { useCurrentRender } from './useCurrentRender';
|
|
43
42
|
import { type ScreenConfigWithParent, useDescriptors } from './useDescriptors';
|
|
@@ -588,7 +587,7 @@ export function useNavigationBuilder<
|
|
|
588
587
|
|
|
589
588
|
const previousRouteKeyListRef = React.useRef(routeKeyList);
|
|
590
589
|
|
|
591
|
-
React.
|
|
590
|
+
React.useInsertionEffect(() => {
|
|
592
591
|
previousRouteKeyListRef.current = routeKeyList;
|
|
593
592
|
});
|
|
594
593
|
|
|
@@ -794,19 +793,6 @@ export function useNavigationBuilder<
|
|
|
794
793
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
795
794
|
}, []);
|
|
796
795
|
|
|
797
|
-
// In some cases (e.g. route names change), internal state might have changed
|
|
798
|
-
// But it hasn't been committed yet, so hasn't propagated to the sync external store
|
|
799
|
-
// During this time, we need to return the internal state in `getState`
|
|
800
|
-
// Otherwise it can result in inconsistent state during render in children
|
|
801
|
-
// To avoid this, we use a ref for render phase, and immediately clear it on commit
|
|
802
|
-
const stateRef = React.useRef<State | null>(state);
|
|
803
|
-
|
|
804
|
-
stateRef.current = state;
|
|
805
|
-
|
|
806
|
-
useClientLayoutEffect(() => {
|
|
807
|
-
stateRef.current = null;
|
|
808
|
-
});
|
|
809
|
-
|
|
810
796
|
const getState = useLatestCallback((): State => {
|
|
811
797
|
const currentState = getCurrentState();
|
|
812
798
|
|
|
@@ -955,9 +941,9 @@ export function useNavigationBuilder<
|
|
|
955
941
|
onAction,
|
|
956
942
|
onUnhandledAction,
|
|
957
943
|
getState,
|
|
944
|
+
state,
|
|
958
945
|
emitter,
|
|
959
946
|
router,
|
|
960
|
-
stateRef,
|
|
961
947
|
});
|
|
962
948
|
|
|
963
949
|
useFocusedListenersChildrenAdapter({
|
|
@@ -1012,7 +998,10 @@ export function useNavigationBuilder<
|
|
|
1012
998
|
return (
|
|
1013
999
|
<NavigationMetaContext.Provider value={undefined}>
|
|
1014
1000
|
<NavigationHelpersContext.Provider value={navigation}>
|
|
1015
|
-
<NavigationStateListenerProvider
|
|
1001
|
+
<NavigationStateListenerProvider
|
|
1002
|
+
state={state}
|
|
1003
|
+
getState={navigation.getState}
|
|
1004
|
+
>
|
|
1016
1005
|
<FocusedRouteKeyContext.Provider
|
|
1017
1006
|
value={state.routes[state.index].key}
|
|
1018
1007
|
>
|
|
@@ -134,7 +134,7 @@ export function useNavigationCache<
|
|
|
134
134
|
[base, getState, navigation, setOptions, emitter]
|
|
135
135
|
);
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
const navigations = state.routes.reduce<
|
|
138
138
|
NavigationCache<State, ScreenOptions, EventMap>
|
|
139
139
|
>((acc, route) => {
|
|
140
140
|
const previous = cache.current[route.key];
|
|
@@ -233,8 +233,12 @@ export function useNavigationCache<
|
|
|
233
233
|
return acc;
|
|
234
234
|
}, {});
|
|
235
235
|
|
|
236
|
+
React.useInsertionEffect(() => {
|
|
237
|
+
cache.current = navigations;
|
|
238
|
+
});
|
|
239
|
+
|
|
236
240
|
return {
|
|
237
241
|
base,
|
|
238
|
-
navigations
|
|
242
|
+
navigations,
|
|
239
243
|
};
|
|
240
244
|
}
|
|
@@ -20,9 +20,9 @@ type Options<State extends NavigationState, Action extends NavigationAction> = {
|
|
|
20
20
|
onAction: (action: NavigationAction) => boolean;
|
|
21
21
|
onUnhandledAction: (action: NavigationAction) => void;
|
|
22
22
|
getState: () => State;
|
|
23
|
+
state: State;
|
|
23
24
|
emitter: NavigationEventEmitter<any>;
|
|
24
25
|
router: Router<State, Action>;
|
|
25
|
-
stateRef: React.RefObject<State | null>;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -39,12 +39,29 @@ export function useNavigationHelpers<
|
|
|
39
39
|
onAction,
|
|
40
40
|
onUnhandledAction,
|
|
41
41
|
getState,
|
|
42
|
+
state,
|
|
42
43
|
emitter,
|
|
43
44
|
router,
|
|
44
|
-
stateRef,
|
|
45
45
|
}: Options<State, Action>) {
|
|
46
46
|
const parentNavigationHelpers = React.useContext(NavigationContext);
|
|
47
47
|
|
|
48
|
+
// In some cases (e.g. route names change), internal state might have changed
|
|
49
|
+
// But it hasn't been committed yet, so hasn't propagated to any listeners
|
|
50
|
+
// During this time, we need to return the internal state in `getState`
|
|
51
|
+
// Otherwise it can return an inconsistent state during render in children
|
|
52
|
+
// This may affect hooks like `useNavigationState` that need to read the state during render
|
|
53
|
+
// To avoid this, we use a ref for render phase, and immediately clear it on commit
|
|
54
|
+
// The ref won't be cleared if the render is discarded, e.g. when interrupted
|
|
55
|
+
// So we also track the stored state the render was based on
|
|
56
|
+
// Then ignore the ref once the stored state changes due to another dispatch
|
|
57
|
+
const stateRef = React.useRef<{ state: State; base: State } | null>(null);
|
|
58
|
+
|
|
59
|
+
stateRef.current = { state, base: getState() };
|
|
60
|
+
|
|
61
|
+
React.useInsertionEffect(() => {
|
|
62
|
+
stateRef.current = null;
|
|
63
|
+
});
|
|
64
|
+
|
|
48
65
|
return React.useMemo(() => {
|
|
49
66
|
const dispatch = (op: Action | ((state: State) => Action)) => {
|
|
50
67
|
const action = typeof op === 'function' ? op(getState()) : op;
|
|
@@ -103,16 +120,16 @@ export function useNavigationHelpers<
|
|
|
103
120
|
return parentNavigationHelpers;
|
|
104
121
|
},
|
|
105
122
|
getState: (): State => {
|
|
123
|
+
const current = getState();
|
|
124
|
+
const pending = stateRef.current;
|
|
125
|
+
|
|
106
126
|
// FIXME: Workaround for when the state is read during render
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Apart from `useSyncExternalStore`, `getState` should never be called during render
|
|
111
|
-
if (stateRef.current != null) {
|
|
112
|
-
return stateRef.current;
|
|
127
|
+
// Apart from subscriptions, `getState` should never be called during render
|
|
128
|
+
if (pending != null && pending.base === current) {
|
|
129
|
+
return pending.state;
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
return
|
|
132
|
+
return current;
|
|
116
133
|
},
|
|
117
134
|
} as NavigationHelpers<ParamListBase, EventMap> & ActionHelpers;
|
|
118
135
|
|
|
@@ -1,7 +1,13 @@
|
|
|
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
|
-
|
|
4
|
+
|
|
5
|
+
import { useClientLayoutEffect } from './useClientLayoutEffect';
|
|
6
|
+
|
|
7
|
+
type NavigationStateListener = {
|
|
8
|
+
getState: () => NavigationState<ParamListBase>;
|
|
9
|
+
subscribe: (callback: () => void) => () => void;
|
|
10
|
+
};
|
|
5
11
|
|
|
6
12
|
type Selector<ParamList extends ParamListBase, T> = (
|
|
7
13
|
state: NavigationState<ParamList>
|
|
@@ -15,36 +21,64 @@ type Selector<ParamList extends ParamListBase, T> = (
|
|
|
15
21
|
export function useNavigationState<ParamList extends ParamListBase, T>(
|
|
16
22
|
selector: Selector<ParamList, T>
|
|
17
23
|
): T {
|
|
18
|
-
|
|
24
|
+
if (typeof selector !== 'function') {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`A selector function must be provided (got ${typeof selector}).`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
const listener = React.useContext(NavigationStateListenerContext);
|
|
31
|
+
|
|
32
|
+
if (listener == null) {
|
|
21
33
|
throw new Error(
|
|
22
34
|
"Couldn't get the navigation state. Is your component inside a navigator?"
|
|
23
35
|
);
|
|
24
36
|
}
|
|
25
37
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
const { getState, subscribe } = listener;
|
|
39
|
+
|
|
40
|
+
const [, forceUpdate] = React.useReducer((count: number) => count + 1, 0);
|
|
41
|
+
|
|
42
|
+
// @ts-expect-error: this is unsafe, but we need to support it for now
|
|
43
|
+
const selected = selector(getState());
|
|
44
|
+
|
|
45
|
+
const selectionRef = React.useRef({ select: selector, selected });
|
|
33
46
|
|
|
34
|
-
|
|
47
|
+
useClientLayoutEffect(() => {
|
|
48
|
+
selectionRef.current = { select: selector, selected };
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
const checkForUpdates = () => {
|
|
53
|
+
const selection = selectionRef.current;
|
|
54
|
+
|
|
55
|
+
// @ts-expect-error: this is unsafe, but we need to support it for now
|
|
56
|
+
if (!Object.is(selection.selected, selection.select(getState()))) {
|
|
57
|
+
forceUpdate();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const unsubscribe = subscribe(checkForUpdates);
|
|
62
|
+
|
|
63
|
+
// The state may have changed between the render and the subscription
|
|
64
|
+
checkForUpdates();
|
|
65
|
+
|
|
66
|
+
return unsubscribe;
|
|
67
|
+
}, [getState, subscribe]);
|
|
68
|
+
|
|
69
|
+
return selected;
|
|
35
70
|
}
|
|
36
71
|
|
|
37
72
|
export function NavigationStateListenerProvider({
|
|
38
73
|
state,
|
|
74
|
+
getState,
|
|
39
75
|
children,
|
|
40
76
|
}: {
|
|
41
77
|
state: NavigationState<ParamListBase>;
|
|
78
|
+
getState: () => NavigationState<ParamListBase>;
|
|
42
79
|
children: React.ReactNode;
|
|
43
80
|
}) {
|
|
44
81
|
const listeners = React.useRef<(() => void)[]>([]);
|
|
45
|
-
const stateRef = React.useRef(state);
|
|
46
|
-
|
|
47
|
-
const getState = useLatestCallback(() => stateRef.current);
|
|
48
82
|
|
|
49
83
|
const subscribe = useLatestCallback((callback: () => void) => {
|
|
50
84
|
listeners.current.push(callback);
|
|
@@ -54,8 +88,8 @@ export function NavigationStateListenerProvider({
|
|
|
54
88
|
};
|
|
55
89
|
});
|
|
56
90
|
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
// Notify subscribers once the new state has committed
|
|
92
|
+
useClientLayoutEffect(() => {
|
|
59
93
|
listeners.current.forEach((callback) => callback());
|
|
60
94
|
}, [state]);
|
|
61
95
|
|
|
@@ -75,9 +109,5 @@ export function NavigationStateListenerProvider({
|
|
|
75
109
|
}
|
|
76
110
|
|
|
77
111
|
const NavigationStateListenerContext = React.createContext<
|
|
78
|
-
|
|
|
79
|
-
getState: () => NavigationState<ParamListBase>;
|
|
80
|
-
subscribe: (callback: () => void) => () => void;
|
|
81
|
-
}
|
|
82
|
-
| undefined
|
|
112
|
+
NavigationStateListener | undefined
|
|
83
113
|
>(undefined);
|
package/src/useOnAction.tsx
CHANGED
|
@@ -60,7 +60,7 @@ export function useOnAction<State extends NavigationState>({
|
|
|
60
60
|
const routerConfigOptionsRef =
|
|
61
61
|
React.useRef<RouterConfigOptions>(routerConfigOptions);
|
|
62
62
|
|
|
63
|
-
React.
|
|
63
|
+
React.useInsertionEffect(() => {
|
|
64
64
|
routerConfigOptionsRef.current = routerConfigOptions;
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -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
|
}
|
|
@@ -5,19 +5,20 @@ import { useClientLayoutEffect } from './useClientLayoutEffect';
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* When screen config changes, we want to update the navigator in the same update phase.
|
|
8
|
-
* However, navigation state is in the root component and React won't let us update it from a child.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* However, navigation state is in the root component and React won't let us update it from a child during render.
|
|
9
|
+
* The render could also be interrupted in concurrent mode, so we need to update after commit.
|
|
10
|
+
* So we queue the update in an insertion effect and flush it in a layout effect.
|
|
11
|
+
* Insertion effects run before layout effects, so every navigator in the tree has queued its update before we flush.
|
|
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
15
|
const { scheduleUpdate, flushUpdates } = React.useContext(
|
|
15
16
|
NavigationBuilderContext
|
|
16
17
|
);
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
React.useInsertionEffect(() => {
|
|
20
|
+
scheduleUpdate(callback);
|
|
21
|
+
});
|
|
21
22
|
|
|
22
23
|
useClientLayoutEffect(flushUpdates);
|
|
23
24
|
}
|
package/src/useSyncState.tsx
CHANGED
|
@@ -87,7 +87,11 @@ export function useSyncState<T>(getInitialState: () => T) {
|
|
|
87
87
|
|
|
88
88
|
if (pendingUpdates.length !== 0) {
|
|
89
89
|
store.batchUpdates(() => {
|
|
90
|
-
//
|
|
90
|
+
// Updates are queued child-first as insertion effects run bottom-up.
|
|
91
|
+
// The root navigator replaces the whole state while nested navigators merge into it.
|
|
92
|
+
// So we apply them root-first to avoid the root clobbering nested updates.
|
|
93
|
+
pendingUpdates.reverse();
|
|
94
|
+
|
|
91
95
|
for (const update of pendingUpdates) {
|
|
92
96
|
update();
|
|
93
97
|
}
|