@react-navigation/core 8.0.0-alpha.34 → 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/NavigationIndependentTree.js +9 -6
- 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/useDescriptors.js +7 -4
- 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/useIsFocused.js +1 -0
- package/lib/module/useIsFocused.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +40 -37
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useNavigationCache.js +82 -78
- package/lib/module/useNavigationCache.js.map +1 -1
- package/lib/module/useNavigationState.js +48 -23
- package/lib/module/useNavigationState.js.map +1 -1
- package/lib/module/useOptionsGetters.js +18 -17
- 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 +3 -1
- 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 +1 -1
- 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/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/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/package.json +2 -2
- package/src/BaseNavigationContainer.tsx +19 -2
- package/src/NavigationBuilderContext.tsx +1 -1
- package/src/NavigationIndependentTree.tsx +16 -8
- package/src/NavigationProvider.tsx +13 -4
- package/src/PreventRemoveProvider.tsx +2 -8
- package/src/SceneView.tsx +3 -1
- package/src/useDescriptors.tsx +18 -5
- package/src/useFocusEffect.tsx +5 -2
- package/src/useFocusEvents.tsx +5 -2
- package/src/useIsFocused.tsx +4 -0
- package/src/useNavigationBuilder.tsx +53 -45
- package/src/useNavigationCache.tsx +127 -105
- package/src/useNavigationState.tsx +60 -29
- package/src/useOptionsGetters.tsx +24 -24
- package/src/usePreventRemoveContext.tsx +1 -1
- package/src/useRouteCache.tsx +49 -33
- package/src/useScheduleUpdate.tsx +4 -1
- package/src/useSyncState.tsx +9 -11
- 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
|
@@ -79,121 +79,143 @@ export function useNavigationCache<
|
|
|
79
79
|
// Cache object which holds navigation objects for each screen
|
|
80
80
|
// We use `React.useMemo` instead of `React.useRef` coz we want to invalidate it when deps change
|
|
81
81
|
// In reality, these deps will rarely change, if ever
|
|
82
|
-
const cache = React.useMemo
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
EventMap,
|
|
88
|
-
ActionHelpers
|
|
89
|
-
>,
|
|
90
|
-
}),
|
|
82
|
+
const cache = React.useMemo<{
|
|
83
|
+
current: NavigationCache<State, ScreenOptions, EventMap, ActionHelpers>;
|
|
84
|
+
routeNames: Record<string, string>;
|
|
85
|
+
}>(
|
|
86
|
+
() => ({ current: {}, routeNames: {} }),
|
|
91
87
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
|
-
[
|
|
88
|
+
[
|
|
89
|
+
getState,
|
|
90
|
+
parentNavigation,
|
|
91
|
+
navigation,
|
|
92
|
+
setOptions,
|
|
93
|
+
router,
|
|
94
|
+
emitter,
|
|
95
|
+
withStackTrace,
|
|
96
|
+
]
|
|
93
97
|
);
|
|
94
98
|
|
|
95
|
-
const next =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
const next = React.useMemo(() => {
|
|
100
|
+
return routes.reduce<
|
|
101
|
+
NavigationCache<State, ScreenOptions, EventMap, ActionHelpers>
|
|
102
|
+
>((acc, route) => {
|
|
103
|
+
const previous =
|
|
104
|
+
cache.routeNames[route.key] === route.name
|
|
105
|
+
? cache.current[route.key]
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
if (previous) {
|
|
109
|
+
// If a cached navigation object already exists, reuse it
|
|
110
|
+
acc[route.key] = previous;
|
|
111
|
+
} else {
|
|
112
|
+
const dispatch = (
|
|
113
|
+
thunk:
|
|
114
|
+
| NavigationAction
|
|
115
|
+
| ((state: State) => NavigationAction | null | undefined)
|
|
116
|
+
) => {
|
|
117
|
+
withStackTrace(dispatch, () => {
|
|
118
|
+
React.startTransition(() => {
|
|
119
|
+
const action =
|
|
120
|
+
typeof thunk === 'function' ? thunk(getState()) : thunk;
|
|
121
|
+
|
|
122
|
+
if (action != null) {
|
|
123
|
+
navigation.dispatch({ source: route.key, ...action });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
117
126
|
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
dispatch(actions[name](...args))
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
acc[name] = helper;
|
|
135
|
-
|
|
136
|
-
return acc;
|
|
137
|
-
},
|
|
138
|
-
{}
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
142
|
-
const { emit, ...rest } = navigation;
|
|
143
|
-
|
|
144
|
-
acc[route.key] = {
|
|
145
|
-
...rest,
|
|
146
|
-
...helpers,
|
|
147
|
-
// FIXME: too much work to fix the types for now
|
|
148
|
-
...(emitter.create(route.key) as any),
|
|
149
|
-
dispatch,
|
|
150
|
-
getParent: (routeName) => {
|
|
151
|
-
if (routeName === route.name) {
|
|
152
|
-
// If the passed route name is the same as the current route's name,
|
|
153
|
-
// we return the cached navigation object for the route
|
|
154
|
-
return acc[route.key];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (routeName !== undefined) {
|
|
158
|
-
const parent = parentNavigation?.getParent(routeName);
|
|
159
|
-
|
|
160
|
-
if (parent === undefined) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
`Couldn't find a navigation object for '${routeName}' in current or any parent screens. Is your component inside the correct screen?`
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const actions = {
|
|
130
|
+
...router.actionCreators,
|
|
131
|
+
...CommonActions,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const helpers = Object.keys(actions).reduce<Record<string, () => void>>(
|
|
135
|
+
(acc, name) => {
|
|
136
|
+
const helper = (...args: any) =>
|
|
137
|
+
withStackTrace(helper, () =>
|
|
138
|
+
// @ts-expect-error: name is a valid key, but TypeScript is dumb
|
|
139
|
+
dispatch(actions[name](...args))
|
|
163
140
|
);
|
|
141
|
+
|
|
142
|
+
acc[name] = helper;
|
|
143
|
+
|
|
144
|
+
return acc;
|
|
145
|
+
},
|
|
146
|
+
{}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
150
|
+
const { emit, ...rest } = navigation;
|
|
151
|
+
|
|
152
|
+
acc[route.key] = {
|
|
153
|
+
...rest,
|
|
154
|
+
...helpers,
|
|
155
|
+
// FIXME: too much work to fix the types for now
|
|
156
|
+
...(emitter.create(route.key) as any),
|
|
157
|
+
dispatch,
|
|
158
|
+
getParent: (routeName) => {
|
|
159
|
+
if (routeName === route.name) {
|
|
160
|
+
// If the passed route name is the same as the current route's name,
|
|
161
|
+
// we return the cached navigation object for the route
|
|
162
|
+
return acc[route.key];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (routeName !== undefined) {
|
|
166
|
+
const parent = parentNavigation?.getParent(routeName);
|
|
167
|
+
|
|
168
|
+
if (parent === undefined) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Couldn't find a navigation object for '${routeName}' in current or any parent screens. Is your component inside the correct screen?`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return parent;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return parentNavigation;
|
|
178
|
+
},
|
|
179
|
+
setOptions: (options: Partial<ScreenOptions>) => {
|
|
180
|
+
setOptions((o) => ({
|
|
181
|
+
...o,
|
|
182
|
+
[route.key]: { ...o[route.key], ...options },
|
|
183
|
+
}));
|
|
184
|
+
},
|
|
185
|
+
isFocused: () => {
|
|
186
|
+
const state = rest.getState();
|
|
187
|
+
|
|
188
|
+
if (state.routes[state.index]?.key !== route.key) {
|
|
189
|
+
return false;
|
|
164
190
|
}
|
|
165
191
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return navigation ? navigation.isFocused() : true;
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return acc;
|
|
192
|
-
}, {});
|
|
192
|
+
// If the current screen is focused, we also need to check if parent navigator is focused
|
|
193
|
+
// This makes sure that we return the focus state in the whole tree, not just this navigator
|
|
194
|
+
return navigation ? navigation.isFocused() : true;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return acc;
|
|
200
|
+
}, {});
|
|
201
|
+
}, [
|
|
202
|
+
cache,
|
|
203
|
+
routes,
|
|
204
|
+
getState,
|
|
205
|
+
navigation,
|
|
206
|
+
router,
|
|
207
|
+
withStackTrace,
|
|
208
|
+
emitter,
|
|
209
|
+
parentNavigation,
|
|
210
|
+
setOptions,
|
|
211
|
+
]);
|
|
193
212
|
|
|
194
213
|
React.useInsertionEffect(() => {
|
|
195
214
|
cache.current = next;
|
|
196
|
-
|
|
215
|
+
cache.routeNames = Object.fromEntries(
|
|
216
|
+
routes.map((route) => [route.key, route.name])
|
|
217
|
+
);
|
|
218
|
+
}, [cache, next, routes]);
|
|
197
219
|
|
|
198
220
|
return next;
|
|
199
221
|
}
|
|
@@ -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
|
|
|
@@ -1,17 +1,22 @@
|
|
|
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>
|
|
@@ -23,24 +28,13 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
|
|
|
23
28
|
NavigationStateContext
|
|
24
29
|
);
|
|
25
30
|
|
|
26
|
-
const optionsChangeListener = React.useCallback(() => {
|
|
27
|
-
const isFocused = navigation?.isFocused() ?? true;
|
|
28
|
-
const hasChildren = Object.keys(optionsGettersFromChildRef.current).length;
|
|
29
|
-
|
|
30
|
-
if (isFocused && !hasChildren) {
|
|
31
|
-
onOptionsChange?.(optionsRef.current ?? {});
|
|
32
|
-
}
|
|
33
|
-
}, [navigation, onOptionsChange]);
|
|
34
|
-
|
|
35
31
|
React.useInsertionEffect(() => {
|
|
36
32
|
optionsRef.current = options;
|
|
37
33
|
}, [options]);
|
|
38
34
|
|
|
39
35
|
React.useEffect(() => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return navigation?.addListener('focus', optionsChangeListener);
|
|
43
|
-
}, [navigation, options, optionsChangeListener]);
|
|
36
|
+
onOptionsChange?.();
|
|
37
|
+
}, [isFocused, onOptionsChange, options]);
|
|
44
38
|
|
|
45
39
|
const getOptionsFromListener = React.useCallback(() => {
|
|
46
40
|
for (const key in optionsGettersFromChildRef.current) {
|
|
@@ -58,9 +52,11 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
|
|
|
58
52
|
}, []);
|
|
59
53
|
|
|
60
54
|
const getCurrentOptions = React.useCallback(() => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
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) {
|
|
64
60
|
return null;
|
|
65
61
|
}
|
|
66
62
|
|
|
@@ -71,7 +67,7 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
|
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
return optionsRef.current;
|
|
74
|
-
}, [
|
|
70
|
+
}, [key, getFocusedRouteKey, getOptionsFromListener]);
|
|
75
71
|
|
|
76
72
|
React.useInsertionEffect(() => {
|
|
77
73
|
// We don't have a parent at the root
|
|
@@ -83,15 +79,19 @@ export function useOptionsGetters({ key, options, navigation }: Options) {
|
|
|
83
79
|
const addOptionsGetter = React.useCallback(
|
|
84
80
|
(key: string, getter: () => object | undefined | null) => {
|
|
85
81
|
optionsGettersFromChildRef.current[key] = getter;
|
|
86
|
-
optionsChangeListener();
|
|
87
82
|
|
|
88
83
|
return () => {
|
|
89
84
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
90
85
|
delete optionsGettersFromChildRef.current[key];
|
|
91
|
-
|
|
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
|
+
}
|
|
92
92
|
};
|
|
93
93
|
},
|
|
94
|
-
[
|
|
94
|
+
[onOptionsChange]
|
|
95
95
|
);
|
|
96
96
|
|
|
97
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,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import useLatestCallback from 'use-latest-callback';
|
|
2
3
|
|
|
3
4
|
import { useNavigationBuilderContext } from './NavigationBuilderContext';
|
|
4
5
|
|
|
@@ -13,8 +14,10 @@ import { useNavigationBuilderContext } from './NavigationBuilderContext';
|
|
|
13
14
|
export function useScheduleUpdate(callback: () => void) {
|
|
14
15
|
const { scheduleUpdate, flushUpdates } = useNavigationBuilderContext();
|
|
15
16
|
|
|
17
|
+
const latestCallback = useLatestCallback(callback);
|
|
18
|
+
|
|
16
19
|
React.useInsertionEffect(() => {
|
|
17
|
-
scheduleUpdate(
|
|
20
|
+
scheduleUpdate(latestCallback);
|
|
18
21
|
});
|
|
19
22
|
|
|
20
23
|
React.useLayoutEffect(flushUpdates);
|