@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.
Files changed (54) hide show
  1. package/lib/module/BaseNavigationContainer.js +4 -2
  2. package/lib/module/BaseNavigationContainer.js.map +1 -1
  3. package/lib/module/NavigationIndependentTree.js +13 -10
  4. package/lib/module/NavigationIndependentTree.js.map +1 -1
  5. package/lib/module/NavigationStateContext.js +3 -0
  6. package/lib/module/NavigationStateContext.js.map +1 -1
  7. package/lib/module/SceneView.js +3 -1
  8. package/lib/module/SceneView.js.map +1 -1
  9. package/lib/module/findFocusedRoute.js.map +1 -1
  10. package/lib/module/getActionFromState.js.map +1 -1
  11. package/lib/module/getStateFromPath.js +15 -16
  12. package/lib/module/getStateFromPath.js.map +1 -1
  13. package/lib/module/getStateFromRouteParams.js +0 -1
  14. package/lib/module/getStateFromRouteParams.js.map +1 -1
  15. package/lib/module/useDescriptors.js +2 -0
  16. package/lib/module/useDescriptors.js.map +1 -1
  17. package/lib/module/useNavigationBuilder.js +3 -0
  18. package/lib/module/useNavigationBuilder.js.map +1 -1
  19. package/lib/module/useNavigationState.js +79 -34
  20. package/lib/module/useNavigationState.js.map +1 -1
  21. package/lib/module/useSyncState.js +25 -6
  22. package/lib/module/useSyncState.js.map +1 -1
  23. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  24. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  25. package/lib/typescript/src/NavigationStateContext.d.ts +1 -0
  26. package/lib/typescript/src/NavigationStateContext.d.ts.map +1 -1
  27. package/lib/typescript/src/SceneView.d.ts +2 -1
  28. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  29. package/lib/typescript/src/findFocusedRoute.d.ts +3 -8
  30. package/lib/typescript/src/findFocusedRoute.d.ts.map +1 -1
  31. package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
  32. package/lib/typescript/src/getStateFromPath.d.ts +1 -4
  33. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  34. package/lib/typescript/src/getStateFromRouteParams.d.ts.map +1 -1
  35. package/lib/typescript/src/useDescriptors.d.ts +2 -1
  36. package/lib/typescript/src/useDescriptors.d.ts.map +1 -1
  37. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  38. package/lib/typescript/src/useNavigationState.d.ts +6 -2
  39. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  40. package/lib/typescript/src/useSyncState.d.ts +1 -0
  41. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  42. package/package.json +5 -5
  43. package/src/BaseNavigationContainer.tsx +13 -3
  44. package/src/NavigationIndependentTree.tsx +21 -16
  45. package/src/NavigationStateContext.tsx +4 -0
  46. package/src/SceneView.tsx +4 -0
  47. package/src/findFocusedRoute.tsx +7 -12
  48. package/src/getActionFromState.tsx +2 -1
  49. package/src/getStateFromPath.tsx +31 -43
  50. package/src/getStateFromRouteParams.tsx +0 -1
  51. package/src/useDescriptors.tsx +3 -0
  52. package/src/useNavigationBuilder.tsx +3 -0
  53. package/src/useNavigationState.tsx +96 -31
  54. 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.30",
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.16",
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.4"
49
+ "use-latest-callback": "^0.3.5"
50
50
  },
51
51
  "devDependencies": {
52
- "@jest/globals": "^29.7.0",
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": "3fe7f6011dbf6d4e72cecac211f2ca78ecab2260"
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 = state?.key ?? keyedListeners.getState.root?.().key;
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
- [state, getState, setState, getKey, setKey, getIsInitial, addOptionsGetter]
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 { NamedNavigationStateListenerListContext } from './useNavigationState';
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
- <NamedNavigationStateListenerListContext.Provider value={undefined}>
24
- <NamedRouteContextListContext.Provider value={undefined}>
25
- <NavigationRouteContext.Provider value={undefined}>
26
- <NavigationContext.Provider value={undefined}>
27
- <NavigationFocusedRouteStateContext.Provider value={undefined}>
28
- <IsFocusedContext.Provider value={undefined}>
29
- <NavigationIndependentTreeContext.Provider value={true}>
30
- {children}
31
- </NavigationIndependentTreeContext.Provider>
32
- </IsFocusedContext.Provider>
33
- </NavigationFocusedRouteStateContext.Provider>
34
- </NavigationContext.Provider>
35
- </NavigationRouteContext.Provider>
36
- </NamedRouteContextListContext.Provider>
37
- </NamedNavigationStateListenerListContext.Provider>
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,
@@ -1,19 +1,14 @@
1
- import type { InitialState } from '@react-navigation/routers';
1
+ import type { NavigationState, PartialState } from '@react-navigation/routers';
2
2
 
3
- type Result =
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(state: InitialState): Result {
13
- let current: InitialState | undefined = state;
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: InitialState['routes'][number] | undefined =
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 = route.state;
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
 
@@ -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
- ): ResultState | undefined {
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
- ): InitialState => {
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
- ): InitialState | undefined => {
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: InitialState = createStateObject(
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
 
@@ -44,7 +44,6 @@ export function getStateFromRouteParams(
44
44
  'path' in params && typeof params.path === 'string'
45
45
  ? params.path
46
46
  : undefined,
47
- // @ts-expect-error this is fine 🔥
48
47
  state:
49
48
  'params' in params &&
50
49
  typeof params.params === 'object' &&
@@ -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
- subscribe: (callback: () => void) => () => void;
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
- const [, forceUpdate] = React.useReducer((count: number) => count + 1, 0);
104
+ return { listener, state };
105
+ },
106
+ undefined,
107
+ () => ({ listener, state: getState() })
108
+ );
80
109
 
81
- const selected = select(getState());
110
+ let state = entry.state;
82
111
 
83
- const selectionRef = React.useRef({ select, selected });
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.useLayoutEffect(() => {
86
- selectionRef.current = { select, selected };
118
+ React.useInsertionEffect(() => {
119
+ committedRef.current = { entry, state, select };
87
120
  });
88
121
 
89
- React.useEffect(() => {
90
- const checkForUpdates = () => {
91
- const selection = selectionRef.current;
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(selection.selected, selection.select(getState()))) {
94
- forceUpdate();
146
+ if (state !== target && !Object.is(select(state), select(target))) {
147
+ dispatch(event);
95
148
  }
96
149
  };
97
150
 
98
- const unsubscribe = subscribe(checkForUpdates);
151
+ const unsubscribe = subscribe(check);
99
152
 
100
- // The state may have changed between the render and the subscription
101
- checkForUpdates();
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 selected;
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 listeners = React.useRef<(() => void)[]>([]);
174
+ const snapshotRef = React.useRef(state);
119
175
 
120
- const subscribe = useLatestCallback((callback: () => void) => {
121
- listeners.current.push(callback);
176
+ React.useInsertionEffect(() => {
177
+ snapshotRef.current = state;
178
+ }, [state]);
122
179
 
123
- return () => {
124
- listeners.current = listeners.current.filter((cb) => cb !== callback);
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.current.forEach((callback) => callback());
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
- subscribe,
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
 
@@ -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
- listeners.forEach((listener) => listener());
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
- if (didUpdate) {
51
- didUpdate = false;
52
- listeners.forEach((listener) => listener());
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;