@react-navigation/core 8.0.0-alpha.30 → 8.0.0-alpha.31
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 +4 -2
- package/lib/module/BaseNavigationContainer.js.map +1 -1
- package/lib/module/NavigationIndependentTree.js +13 -10
- package/lib/module/NavigationIndependentTree.js.map +1 -1
- package/lib/module/NavigationStateContext.js +3 -0
- package/lib/module/NavigationStateContext.js.map +1 -1
- package/lib/module/SceneView.js +3 -1
- package/lib/module/SceneView.js.map +1 -1
- package/lib/module/findFocusedRoute.js.map +1 -1
- package/lib/module/getActionFromState.js.map +1 -1
- package/lib/module/getStateFromPath.js +15 -16
- package/lib/module/getStateFromPath.js.map +1 -1
- package/lib/module/getStateFromRouteParams.js +0 -1
- package/lib/module/getStateFromRouteParams.js.map +1 -1
- package/lib/module/useDescriptors.js +2 -0
- package/lib/module/useDescriptors.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +3 -0
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useNavigationState.js +79 -34
- package/lib/module/useNavigationState.js.map +1 -1
- package/lib/module/useSyncState.js +25 -6
- package/lib/module/useSyncState.js.map +1 -1
- package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
- package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
- package/lib/typescript/src/NavigationStateContext.d.ts +1 -0
- package/lib/typescript/src/NavigationStateContext.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/findFocusedRoute.d.ts +3 -8
- package/lib/typescript/src/findFocusedRoute.d.ts.map +1 -1
- package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromPath.d.ts +1 -4
- package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromRouteParams.d.ts.map +1 -1
- package/lib/typescript/src/useDescriptors.d.ts +2 -1
- package/lib/typescript/src/useDescriptors.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationState.d.ts +6 -2
- package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
- package/lib/typescript/src/useSyncState.d.ts +1 -0
- package/lib/typescript/src/useSyncState.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/BaseNavigationContainer.tsx +13 -3
- package/src/NavigationIndependentTree.tsx +21 -16
- package/src/NavigationStateContext.tsx +4 -0
- package/src/SceneView.tsx +4 -0
- package/src/findFocusedRoute.tsx +7 -12
- package/src/getActionFromState.tsx +2 -1
- package/src/getStateFromPath.tsx +31 -43
- package/src/getStateFromRouteParams.tsx +0 -1
- package/src/useDescriptors.tsx +3 -0
- package/src/useNavigationBuilder.tsx +3 -0
- package/src/useNavigationState.tsx +96 -31
- package/src/useSyncState.tsx +27 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-navigation/core",
|
|
3
3
|
"description": "Core utilities for building navigators",
|
|
4
|
-
"version": "8.0.0-alpha.
|
|
4
|
+
"version": "8.0.0-alpha.31",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
7
7
|
"react-native",
|
|
@@ -40,16 +40,16 @@
|
|
|
40
40
|
"clean": "del lib"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@react-navigation/routers": "^8.0.0-alpha.
|
|
43
|
+
"@react-navigation/routers": "^8.0.0-alpha.17",
|
|
44
44
|
"escape-string-regexp": "^5.0.0",
|
|
45
45
|
"fast-deep-equal": "^3.1.3",
|
|
46
46
|
"nanoid": "^5.1.11",
|
|
47
47
|
"query-string": "^9.4.0",
|
|
48
48
|
"react-is": "^19.2.6",
|
|
49
|
-
"use-latest-callback": "^0.3.
|
|
49
|
+
"use-latest-callback": "^0.3.5"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@jest/globals": "^
|
|
52
|
+
"@jest/globals": "^30.4.1",
|
|
53
53
|
"@testing-library/react-native": "^14.0.0",
|
|
54
54
|
"@types/react": "~19.2.17",
|
|
55
55
|
"@types/react-is": "^19.2.0",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
]
|
|
85
85
|
]
|
|
86
86
|
},
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "f8dd4e8fa7b0c7ec159f9f58ab31660aca8f1bf6"
|
|
88
88
|
}
|
|
@@ -104,7 +104,7 @@ export function BaseNavigationContainer<ParamList extends {} = RootParamList>({
|
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
const { state, getState, setState, scheduleUpdate, flushUpdates } =
|
|
107
|
+
const { state, getState, setState, subscribe, scheduleUpdate, flushUpdates } =
|
|
108
108
|
useSyncState<State>(() =>
|
|
109
109
|
getPartialState(initialState == null ? undefined : initialState)
|
|
110
110
|
);
|
|
@@ -161,7 +161,7 @@ export function BaseNavigationContainer<ParamList extends {} = RootParamList>({
|
|
|
161
161
|
|
|
162
162
|
const resetRoot = useLatestCallback(
|
|
163
163
|
(state: PartialState<NavigationState> | NavigationState) => {
|
|
164
|
-
const target =
|
|
164
|
+
const target = keyedListeners.getState.root?.().key;
|
|
165
165
|
const listener = listeners.focus[0];
|
|
166
166
|
|
|
167
167
|
if (target == null || listener == null) {
|
|
@@ -317,12 +317,22 @@ export function BaseNavigationContainer<ParamList extends {} = RootParamList>({
|
|
|
317
317
|
state,
|
|
318
318
|
getState,
|
|
319
319
|
setState,
|
|
320
|
+
subscribe,
|
|
320
321
|
getKey,
|
|
321
322
|
setKey,
|
|
322
323
|
getIsInitial,
|
|
323
324
|
addOptionsGetter,
|
|
324
325
|
}),
|
|
325
|
-
[
|
|
326
|
+
[
|
|
327
|
+
state,
|
|
328
|
+
getState,
|
|
329
|
+
setState,
|
|
330
|
+
subscribe,
|
|
331
|
+
getKey,
|
|
332
|
+
setKey,
|
|
333
|
+
getIsInitial,
|
|
334
|
+
addOptionsGetter,
|
|
335
|
+
]
|
|
326
336
|
);
|
|
327
337
|
|
|
328
338
|
const onReadyRef = React.useRef(onReady);
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
NavigationRouteContext,
|
|
9
9
|
} from './NavigationProvider';
|
|
10
10
|
import { IsFocusedContext } from './useIsFocused';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
NamedNavigationStateListenerListContext,
|
|
13
|
+
NavigationStateListenerContext,
|
|
14
|
+
} from './useNavigationState';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Component to make the child navigation container independent of parent containers.
|
|
@@ -20,20 +23,22 @@ export function NavigationIndependentTree({
|
|
|
20
23
|
}) {
|
|
21
24
|
return (
|
|
22
25
|
// We need to clear any existing contexts for nested independent container to work correctly
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
<NavigationStateListenerContext.Provider value={undefined}>
|
|
27
|
+
<NamedNavigationStateListenerListContext.Provider value={undefined}>
|
|
28
|
+
<NamedRouteContextListContext.Provider value={undefined}>
|
|
29
|
+
<NavigationRouteContext.Provider value={undefined}>
|
|
30
|
+
<NavigationContext.Provider value={undefined}>
|
|
31
|
+
<NavigationFocusedRouteStateContext.Provider value={undefined}>
|
|
32
|
+
<IsFocusedContext.Provider value={undefined}>
|
|
33
|
+
<NavigationIndependentTreeContext.Provider value={true}>
|
|
34
|
+
{children}
|
|
35
|
+
</NavigationIndependentTreeContext.Provider>
|
|
36
|
+
</IsFocusedContext.Provider>
|
|
37
|
+
</NavigationFocusedRouteStateContext.Provider>
|
|
38
|
+
</NavigationContext.Provider>
|
|
39
|
+
</NavigationRouteContext.Provider>
|
|
40
|
+
</NamedRouteContextListContext.Provider>
|
|
41
|
+
</NamedNavigationStateListenerListContext.Provider>
|
|
42
|
+
</NavigationStateListenerContext.Provider>
|
|
38
43
|
);
|
|
39
44
|
}
|
|
@@ -13,6 +13,7 @@ export const NavigationStateContext = React.createContext<{
|
|
|
13
13
|
setState: (
|
|
14
14
|
state: NavigationState | PartialState<NavigationState> | undefined
|
|
15
15
|
) => void;
|
|
16
|
+
subscribe: (callback: () => void) => () => void;
|
|
16
17
|
getIsInitial: () => boolean;
|
|
17
18
|
addOptionsGetter?:
|
|
18
19
|
| ((key: string, getter: () => object | undefined | null) => void)
|
|
@@ -32,6 +33,9 @@ export const NavigationStateContext = React.createContext<{
|
|
|
32
33
|
get setState(): any {
|
|
33
34
|
throw new Error(MISSING_CONTEXT_ERROR);
|
|
34
35
|
},
|
|
36
|
+
get subscribe(): any {
|
|
37
|
+
throw new Error(MISSING_CONTEXT_ERROR);
|
|
38
|
+
},
|
|
35
39
|
get getIsInitial(): any {
|
|
36
40
|
throw new Error(MISSING_CONTEXT_ERROR);
|
|
37
41
|
},
|
package/src/SceneView.tsx
CHANGED
|
@@ -24,6 +24,7 @@ type Props<State extends NavigationState, ScreenOptions extends {}> = {
|
|
|
24
24
|
routeState: NavigationState | PartialState<NavigationState> | undefined;
|
|
25
25
|
getState: () => State;
|
|
26
26
|
setState: (state: State) => void;
|
|
27
|
+
subscribe: (callback: () => void) => () => void;
|
|
27
28
|
options: object;
|
|
28
29
|
clearOptions: () => void;
|
|
29
30
|
};
|
|
@@ -42,6 +43,7 @@ export function SceneView<
|
|
|
42
43
|
routeState,
|
|
43
44
|
getState,
|
|
44
45
|
setState,
|
|
46
|
+
subscribe,
|
|
45
47
|
options,
|
|
46
48
|
clearOptions,
|
|
47
49
|
}: Props<State, ScreenOptions>) {
|
|
@@ -152,6 +154,7 @@ export function SceneView<
|
|
|
152
154
|
state: routeState,
|
|
153
155
|
getState: getCurrentState,
|
|
154
156
|
setState: setCurrentState,
|
|
157
|
+
subscribe,
|
|
155
158
|
getKey,
|
|
156
159
|
setKey,
|
|
157
160
|
getIsInitial,
|
|
@@ -161,6 +164,7 @@ export function SceneView<
|
|
|
161
164
|
routeState,
|
|
162
165
|
getCurrentState,
|
|
163
166
|
setCurrentState,
|
|
167
|
+
subscribe,
|
|
164
168
|
getKey,
|
|
165
169
|
setKey,
|
|
166
170
|
getIsInitial,
|
package/src/findFocusedRoute.tsx
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NavigationState, PartialState } from '@react-navigation/routers';
|
|
2
2
|
|
|
3
|
-
type
|
|
4
|
-
| {
|
|
5
|
-
key?: string | undefined;
|
|
6
|
-
name: string;
|
|
7
|
-
params?: object | undefined;
|
|
8
|
-
path?: string | undefined;
|
|
9
|
-
}
|
|
10
|
-
| undefined;
|
|
3
|
+
type State = NavigationState | PartialState<NavigationState>;
|
|
11
4
|
|
|
12
|
-
export function findFocusedRoute(
|
|
13
|
-
|
|
5
|
+
export function findFocusedRoute(
|
|
6
|
+
state: State
|
|
7
|
+
): State['routes'][number] | undefined {
|
|
8
|
+
let current: State | undefined = state;
|
|
14
9
|
|
|
15
10
|
while (current != null) {
|
|
16
|
-
const route:
|
|
11
|
+
const route: State['routes'][number] | undefined =
|
|
17
12
|
current.routes[current.index ?? current.routes.length - 1];
|
|
18
13
|
|
|
19
14
|
if (route?.state == null) {
|
|
@@ -89,7 +89,8 @@ export function getActionFromState(
|
|
|
89
89
|
return undefined;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
let current: PartialState<NavigationState> | undefined =
|
|
92
|
+
let current: NavigationState | PartialState<NavigationState> | undefined =
|
|
93
|
+
route.state;
|
|
93
94
|
let config: ConfigItem | undefined = normalizedConfig?.screens?.[route.name];
|
|
94
95
|
let params = { ...route.params } as NavigatorScreenParams<ParamListBase>;
|
|
95
96
|
|
package/src/getStateFromPath.tsx
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
InitialState,
|
|
3
|
-
NavigationState,
|
|
4
|
-
PartialState,
|
|
5
|
-
} from '@react-navigation/routers';
|
|
1
|
+
import type { NavigationState, PartialState } from '@react-navigation/routers';
|
|
6
2
|
import escape from 'escape-string-regexp';
|
|
7
3
|
import queryString from 'query-string';
|
|
8
4
|
|
|
9
5
|
import { arrayStartsWith } from './arrayStartsWith';
|
|
10
|
-
import { findFocusedRoute } from './findFocusedRoute';
|
|
11
6
|
import {
|
|
12
7
|
combinePatternParts,
|
|
13
8
|
getPatternParts,
|
|
@@ -58,13 +53,10 @@ type InitialRouteConfig = {
|
|
|
58
53
|
parentScreens: string[];
|
|
59
54
|
};
|
|
60
55
|
|
|
61
|
-
type ResultState = PartialState<NavigationState> & {
|
|
62
|
-
state?: ResultState | undefined;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
56
|
type ParsedRoute = {
|
|
66
57
|
name: string;
|
|
67
58
|
params?: Record<string, unknown> | undefined;
|
|
59
|
+
path?: string | undefined;
|
|
68
60
|
};
|
|
69
61
|
|
|
70
62
|
type RoutePatternPart = PatternPart & { screen: string };
|
|
@@ -177,7 +169,7 @@ export function getStateFromPath<ParamList extends {}>(
|
|
|
177
169
|
path: string,
|
|
178
170
|
options?: Options<ParamList>,
|
|
179
171
|
previous?: NavigationState
|
|
180
|
-
):
|
|
172
|
+
): PartialState<NavigationState> | undefined {
|
|
181
173
|
const {
|
|
182
174
|
initialRoutes,
|
|
183
175
|
configs,
|
|
@@ -1016,7 +1008,7 @@ const createStateObject = (
|
|
|
1016
1008
|
initialRoute: string | undefined,
|
|
1017
1009
|
route: ParsedRoute,
|
|
1018
1010
|
isEmpty: boolean
|
|
1019
|
-
):
|
|
1011
|
+
): PartialState<NavigationState> => {
|
|
1020
1012
|
if (isEmpty) {
|
|
1021
1013
|
if (initialRoute) {
|
|
1022
1014
|
return {
|
|
@@ -1047,7 +1039,32 @@ const createNestedStateObject = (
|
|
|
1047
1039
|
routes: ParsedRoute[],
|
|
1048
1040
|
initialRoutes: InitialRouteConfig[],
|
|
1049
1041
|
routeConfig?: RouteConfig
|
|
1050
|
-
):
|
|
1042
|
+
): PartialState<NavigationState> | undefined => {
|
|
1043
|
+
const focusedRoute = routes[routes.length - 1];
|
|
1044
|
+
|
|
1045
|
+
if (focusedRoute == null) {
|
|
1046
|
+
return undefined;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
focusedRoute.path = path.replace(/\/$/, '');
|
|
1050
|
+
|
|
1051
|
+
const queryParams = parseQueryParams(
|
|
1052
|
+
path,
|
|
1053
|
+
routeConfig?.parse,
|
|
1054
|
+
routeConfig?.pathParamNames,
|
|
1055
|
+
routeConfig?.explicitParamNames,
|
|
1056
|
+
routeConfig?.hasNestedScreens,
|
|
1057
|
+
focusedRoute.params
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
if (!queryParams.valid) {
|
|
1061
|
+
return undefined;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (queryParams.params) {
|
|
1065
|
+
focusedRoute.params = { ...focusedRoute.params, ...queryParams.params };
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1051
1068
|
let route = routes.shift();
|
|
1052
1069
|
|
|
1053
1070
|
if (route == null) {
|
|
@@ -1060,11 +1077,7 @@ const createNestedStateObject = (
|
|
|
1060
1077
|
|
|
1061
1078
|
parentScreens.push(route.name);
|
|
1062
1079
|
|
|
1063
|
-
const state
|
|
1064
|
-
initialRoute,
|
|
1065
|
-
route,
|
|
1066
|
-
routes.length === 0
|
|
1067
|
-
);
|
|
1080
|
+
const state = createStateObject(initialRoute, route, routes.length === 0);
|
|
1068
1081
|
|
|
1069
1082
|
if (routes.length > 0) {
|
|
1070
1083
|
let nestedState = state;
|
|
@@ -1104,31 +1117,6 @@ const createNestedStateObject = (
|
|
|
1104
1117
|
}
|
|
1105
1118
|
}
|
|
1106
1119
|
|
|
1107
|
-
const focusedRoute = findFocusedRoute(state);
|
|
1108
|
-
|
|
1109
|
-
if (focusedRoute == null) {
|
|
1110
|
-
return undefined;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
focusedRoute.path = path.replace(/\/$/, '');
|
|
1114
|
-
|
|
1115
|
-
const queryParams = parseQueryParams(
|
|
1116
|
-
path,
|
|
1117
|
-
routeConfig?.parse,
|
|
1118
|
-
routeConfig?.pathParamNames,
|
|
1119
|
-
routeConfig?.explicitParamNames,
|
|
1120
|
-
routeConfig?.hasNestedScreens,
|
|
1121
|
-
focusedRoute.params
|
|
1122
|
-
);
|
|
1123
|
-
|
|
1124
|
-
if (!queryParams.valid) {
|
|
1125
|
-
return undefined;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
if (queryParams.params) {
|
|
1129
|
-
focusedRoute.params = { ...focusedRoute.params, ...queryParams.params };
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
1120
|
return state;
|
|
1133
1121
|
};
|
|
1134
1122
|
|
package/src/useDescriptors.tsx
CHANGED
|
@@ -78,6 +78,7 @@ type Options<
|
|
|
78
78
|
onAction: (action: NavigationAction) => boolean;
|
|
79
79
|
getState: () => State;
|
|
80
80
|
setState: (state: State) => void;
|
|
81
|
+
subscribe: (callback: () => void) => () => void;
|
|
81
82
|
addListener: AddListener;
|
|
82
83
|
addKeyedListener: AddKeyedListener;
|
|
83
84
|
onRouteFocus: (key: string) => void;
|
|
@@ -107,6 +108,7 @@ export function useDescriptors<
|
|
|
107
108
|
onAction,
|
|
108
109
|
getState,
|
|
109
110
|
setState,
|
|
111
|
+
subscribe,
|
|
110
112
|
addListener,
|
|
111
113
|
addKeyedListener,
|
|
112
114
|
onRouteFocus,
|
|
@@ -257,6 +259,7 @@ export function useDescriptors<
|
|
|
257
259
|
routeState={routeState}
|
|
258
260
|
getState={getState}
|
|
259
261
|
setState={setState}
|
|
262
|
+
subscribe={subscribe}
|
|
260
263
|
options={customOptions}
|
|
261
264
|
clearOptions={clearOptions}
|
|
262
265
|
/>
|
|
@@ -453,6 +453,7 @@ export function useNavigationBuilder<
|
|
|
453
453
|
state: currentState,
|
|
454
454
|
getState: getCurrentState,
|
|
455
455
|
setState: setCurrentState,
|
|
456
|
+
subscribe,
|
|
456
457
|
setKey,
|
|
457
458
|
getKey,
|
|
458
459
|
getIsInitial,
|
|
@@ -1033,6 +1034,7 @@ export function useNavigationBuilder<
|
|
|
1033
1034
|
onAction,
|
|
1034
1035
|
getState,
|
|
1035
1036
|
setState,
|
|
1037
|
+
subscribe,
|
|
1036
1038
|
onRouteFocus,
|
|
1037
1039
|
addListener,
|
|
1038
1040
|
addKeyedListener,
|
|
@@ -1064,6 +1066,7 @@ export function useNavigationBuilder<
|
|
|
1064
1066
|
<NavigationStateListenerProvider
|
|
1065
1067
|
state={state}
|
|
1066
1068
|
getState={navigation.getState}
|
|
1069
|
+
subscribe={subscribe}
|
|
1067
1070
|
>
|
|
1068
1071
|
<FocusedRouteKeyContext.Provider value={focusedRoute.key}>
|
|
1069
1072
|
<PreventRemoveProvider>{element}</PreventRemoveProvider>
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { NavigationState, ParamListBase } from '@react-navigation/routers';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import useLatestCallback from 'use-latest-callback';
|
|
4
3
|
|
|
5
4
|
import type { NavigationListForNested, RootNavigator } from './types';
|
|
6
5
|
|
|
6
|
+
type NavigationStateEvent = 'update' | 'commit';
|
|
7
|
+
|
|
7
8
|
type NavigationStateListener = {
|
|
8
9
|
getState: () => NavigationState<ParamListBase>;
|
|
9
|
-
|
|
10
|
+
getSnapshot: () => NavigationState<ParamListBase>;
|
|
11
|
+
subscribe: (callback: (event: NavigationStateEvent) => void) => () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type NavigationStateEntry = {
|
|
15
|
+
listener: NavigationStateListener;
|
|
16
|
+
state: NavigationState<ParamListBase>;
|
|
10
17
|
};
|
|
11
18
|
|
|
12
19
|
type NavigationStateForNested<
|
|
@@ -74,68 +81,126 @@ export function useNavigationState(...args: unknown[]): unknown {
|
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
const { getState, subscribe } = listener;
|
|
84
|
+
const { getState, getSnapshot, subscribe } = listener;
|
|
85
|
+
|
|
86
|
+
// The store can contain state from a transition that hasn't committed yet.
|
|
87
|
+
// Reading it during render can show state that doesn't match the navigator in that render.
|
|
88
|
+
// The reducer lets React process state in the same render lane as the navigator.
|
|
89
|
+
const [entry, dispatch] = React.useReducer(
|
|
90
|
+
(previous: NavigationStateEntry, event: NavigationStateEvent) => {
|
|
91
|
+
// Navigation state can change between the dispatch and the reducer running.
|
|
92
|
+
// Reading here ensures the processed event uses the state for that render.
|
|
93
|
+
const state = event === 'update' ? getState() : getSnapshot();
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
previous.listener === listener &&
|
|
97
|
+
(previous.state === state ||
|
|
98
|
+
(event === 'update' &&
|
|
99
|
+
Object.is(select(previous.state), select(state))))
|
|
100
|
+
) {
|
|
101
|
+
return previous;
|
|
102
|
+
}
|
|
78
103
|
|
|
79
|
-
|
|
104
|
+
return { listener, state };
|
|
105
|
+
},
|
|
106
|
+
undefined,
|
|
107
|
+
() => ({ listener, state: getState() })
|
|
108
|
+
);
|
|
80
109
|
|
|
81
|
-
|
|
110
|
+
let state = entry.state;
|
|
82
111
|
|
|
83
|
-
const
|
|
112
|
+
const committedRef = React.useRef<{
|
|
113
|
+
entry: NavigationStateEntry | null;
|
|
114
|
+
state: NavigationState<ParamListBase>;
|
|
115
|
+
select: (state: NavigationState<ParamListBase>) => unknown;
|
|
116
|
+
}>({ entry: null, state, select });
|
|
84
117
|
|
|
85
|
-
React.
|
|
86
|
-
|
|
118
|
+
React.useInsertionEffect(() => {
|
|
119
|
+
committedRef.current = { entry, state, select };
|
|
87
120
|
});
|
|
88
121
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
React.useLayoutEffect(() => {
|
|
139
|
+
const check = (event: NavigationStateEvent) => {
|
|
140
|
+
const { state, select } = committedRef.current;
|
|
141
|
+
|
|
142
|
+
// An 'update' event follows the latest state in the lane the update was made in.
|
|
143
|
+
// A 'commit' event syncs the reducer to the state committed by the navigator.
|
|
144
|
+
const target = event === 'update' ? getState() : getSnapshot();
|
|
92
145
|
|
|
93
|
-
if (!Object.is(
|
|
94
|
-
|
|
146
|
+
if (state !== target && !Object.is(select(state), select(target))) {
|
|
147
|
+
dispatch(event);
|
|
95
148
|
}
|
|
96
149
|
};
|
|
97
150
|
|
|
98
|
-
const unsubscribe = subscribe(
|
|
151
|
+
const unsubscribe = subscribe(check);
|
|
99
152
|
|
|
100
|
-
// The
|
|
101
|
-
|
|
153
|
+
// The reducer may not match the committed state when the subscription starts.
|
|
154
|
+
// This happens on mount during a transition or an update before subscribing.
|
|
155
|
+
check('commit');
|
|
102
156
|
|
|
103
157
|
return unsubscribe;
|
|
104
|
-
}, [getState, subscribe]);
|
|
158
|
+
}, [getState, getSnapshot, subscribe]);
|
|
105
159
|
|
|
106
|
-
return
|
|
160
|
+
return select(state);
|
|
107
161
|
}
|
|
108
162
|
|
|
109
163
|
export function NavigationStateListenerProvider({
|
|
110
164
|
state,
|
|
111
165
|
getState,
|
|
166
|
+
subscribe,
|
|
112
167
|
children,
|
|
113
168
|
}: {
|
|
114
169
|
state: NavigationState<ParamListBase>;
|
|
115
170
|
getState: () => NavigationState<ParamListBase>;
|
|
171
|
+
subscribe: (callback: () => void) => () => void;
|
|
116
172
|
children: React.ReactNode;
|
|
117
173
|
}) {
|
|
118
|
-
const
|
|
174
|
+
const snapshotRef = React.useRef(state);
|
|
119
175
|
|
|
120
|
-
|
|
121
|
-
|
|
176
|
+
React.useInsertionEffect(() => {
|
|
177
|
+
snapshotRef.current = state;
|
|
178
|
+
}, [state]);
|
|
122
179
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
180
|
+
const [listeners] = React.useState(
|
|
181
|
+
() => new Set<(event: NavigationStateEvent) => void>()
|
|
182
|
+
);
|
|
127
183
|
|
|
128
|
-
// Notify subscribers once the new state has committed
|
|
129
184
|
React.useLayoutEffect(() => {
|
|
130
|
-
listeners.
|
|
131
|
-
}, [state]);
|
|
185
|
+
listeners.forEach((listener) => listener('commit'));
|
|
186
|
+
}, [listeners, state]);
|
|
132
187
|
|
|
133
188
|
const context = React.useMemo(
|
|
134
189
|
() => ({
|
|
135
190
|
getState,
|
|
136
|
-
|
|
191
|
+
getSnapshot: () => snapshotRef.current,
|
|
192
|
+
subscribe: (callback: (event: NavigationStateEvent) => void) => {
|
|
193
|
+
const unsubscribe = subscribe(() => callback('update'));
|
|
194
|
+
|
|
195
|
+
listeners.add(callback);
|
|
196
|
+
|
|
197
|
+
return () => {
|
|
198
|
+
listeners.delete(callback);
|
|
199
|
+
unsubscribe();
|
|
200
|
+
};
|
|
201
|
+
},
|
|
137
202
|
}),
|
|
138
|
-
[getState, subscribe]
|
|
203
|
+
[getState, listeners, subscribe]
|
|
139
204
|
);
|
|
140
205
|
|
|
141
206
|
return (
|
|
@@ -177,7 +242,7 @@ export function NamedNavigationStateListenerProvider({
|
|
|
177
242
|
);
|
|
178
243
|
}
|
|
179
244
|
|
|
180
|
-
const NavigationStateListenerContext = React.createContext<
|
|
245
|
+
export const NavigationStateListenerContext = React.createContext<
|
|
181
246
|
NavigationStateListener | undefined
|
|
182
247
|
>(undefined);
|
|
183
248
|
|
package/src/useSyncState.tsx
CHANGED
|
@@ -24,13 +24,29 @@ const createStore = <T,>(getInitialState: () => T) => {
|
|
|
24
24
|
let isBatching = false;
|
|
25
25
|
let didUpdate = false;
|
|
26
26
|
|
|
27
|
+
const notify = () => {
|
|
28
|
+
let error: { value: unknown } | undefined;
|
|
29
|
+
|
|
30
|
+
listeners.forEach((listener) => {
|
|
31
|
+
try {
|
|
32
|
+
listener();
|
|
33
|
+
} catch (e) {
|
|
34
|
+
error = error ?? { value: e };
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (error) {
|
|
39
|
+
throw error.value;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
27
43
|
const setState = (newState: T) => {
|
|
28
44
|
state = deepFreeze(newState);
|
|
29
45
|
|
|
30
46
|
if (isBatching) {
|
|
31
47
|
didUpdate = true;
|
|
32
48
|
} else {
|
|
33
|
-
|
|
49
|
+
notify();
|
|
34
50
|
}
|
|
35
51
|
};
|
|
36
52
|
|
|
@@ -44,12 +60,16 @@ const createStore = <T,>(getInitialState: () => T) => {
|
|
|
44
60
|
|
|
45
61
|
const batchUpdates = (callback: () => void) => {
|
|
46
62
|
isBatching = true;
|
|
47
|
-
callback();
|
|
48
|
-
isBatching = false;
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
try {
|
|
65
|
+
callback();
|
|
66
|
+
} finally {
|
|
67
|
+
isBatching = false;
|
|
68
|
+
|
|
69
|
+
if (didUpdate) {
|
|
70
|
+
didUpdate = false;
|
|
71
|
+
notify();
|
|
72
|
+
}
|
|
53
73
|
}
|
|
54
74
|
};
|
|
55
75
|
|
|
@@ -119,6 +139,7 @@ export function useSyncState<T>(getInitialState: () => T) {
|
|
|
119
139
|
state,
|
|
120
140
|
getState: store.getState,
|
|
121
141
|
setState: store.setState,
|
|
142
|
+
subscribe: store.subscribe,
|
|
122
143
|
scheduleUpdate,
|
|
123
144
|
flushUpdates,
|
|
124
145
|
} as const;
|