1 | import {
|
2 | CommonActions,
|
3 | InitialState,
|
4 | NavigationAction,
|
5 | NavigationState,
|
6 | ParamListBase,
|
7 | PartialState,
|
8 | Route,
|
9 | } from '@react-navigation/routers';
|
10 | import * as React from 'react';
|
11 |
|
12 | import checkDuplicateRouteNames from './checkDuplicateRouteNames';
|
13 | import checkSerializable from './checkSerializable';
|
14 | import { NOT_INITIALIZED_ERROR } from './createNavigationContainerRef';
|
15 | import EnsureSingleNavigator from './EnsureSingleNavigator';
|
16 | import findFocusedRoute from './findFocusedRoute';
|
17 | import NavigationBuilderContext from './NavigationBuilderContext';
|
18 | import NavigationContainerRefContext from './NavigationContainerRefContext';
|
19 | import NavigationContext from './NavigationContext';
|
20 | import NavigationRouteContext from './NavigationRouteContext';
|
21 | import NavigationStateContext from './NavigationStateContext';
|
22 | import type {
|
23 | NavigationContainerEventMap,
|
24 | NavigationContainerProps,
|
25 | NavigationContainerRef,
|
26 | } from './types';
|
27 | import UnhandledActionContext from './UnhandledActionContext';
|
28 | import useChildListeners from './useChildListeners';
|
29 | import useEventEmitter from './useEventEmitter';
|
30 | import useKeyedChildListeners from './useKeyedChildListeners';
|
31 | import useOptionsGetters from './useOptionsGetters';
|
32 | import { ScheduleUpdateContext } from './useScheduleUpdate';
|
33 | import useSyncState from './useSyncState';
|
34 |
|
35 | type State = NavigationState | PartialState<NavigationState> | undefined;
|
36 |
|
37 | const serializableWarnings: string[] = [];
|
38 | const duplicateNameWarnings: string[] = [];
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 | const getPartialState = (
|
46 | state: InitialState | undefined
|
47 | ): PartialState<NavigationState> | undefined => {
|
48 | if (state === undefined) {
|
49 | return;
|
50 | }
|
51 |
|
52 |
|
53 | const { key, routeNames, ...partialState } = state;
|
54 |
|
55 | return {
|
56 | ...partialState,
|
57 | stale: true,
|
58 | routes: state.routes.map((route) => {
|
59 | if (route.state === undefined) {
|
60 | return route as Route<string> & {
|
61 | state?: PartialState<NavigationState>;
|
62 | };
|
63 | }
|
64 |
|
65 | return { ...route, state: getPartialState(route.state) };
|
66 | }),
|
67 | };
|
68 | };
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | const BaseNavigationContainer = React.forwardRef(
|
80 | function BaseNavigationContainer(
|
81 | {
|
82 | initialState,
|
83 | onStateChange,
|
84 | onUnhandledAction,
|
85 | independent,
|
86 | children,
|
87 | }: NavigationContainerProps,
|
88 | ref?: React.Ref<NavigationContainerRef<ParamListBase>>
|
89 | ) {
|
90 | const parent = React.useContext(NavigationStateContext);
|
91 |
|
92 | if (!parent.isDefault && !independent) {
|
93 | throw new Error(
|
94 | "Looks like you have nested a 'NavigationContainer' inside another. Normally you need only one container at the root of the app, so this was probably an error. If this was intentional, pass 'independent={true}' explicitly. Note that this will make the child navigators disconnected from the parent and you won't be able to navigate between them."
|
95 | );
|
96 | }
|
97 |
|
98 | const [state, getState, setState, scheduleUpdate, flushUpdates] =
|
99 | useSyncState<State>(() =>
|
100 | getPartialState(initialState == null ? undefined : initialState)
|
101 | );
|
102 |
|
103 | const isFirstMountRef = React.useRef<boolean>(true);
|
104 |
|
105 | const navigatorKeyRef = React.useRef<string | undefined>();
|
106 |
|
107 | const getKey = React.useCallback(() => navigatorKeyRef.current, []);
|
108 |
|
109 | const setKey = React.useCallback((key: string) => {
|
110 | navigatorKeyRef.current = key;
|
111 | }, []);
|
112 |
|
113 | const { listeners, addListener } = useChildListeners();
|
114 |
|
115 | const { keyedListeners, addKeyedListener } = useKeyedChildListeners();
|
116 |
|
117 | const dispatch = React.useCallback(
|
118 | (
|
119 | action:
|
120 | | NavigationAction
|
121 | | ((state: NavigationState) => NavigationAction)
|
122 | ) => {
|
123 | if (listeners.focus[0] == null) {
|
124 | console.error(NOT_INITIALIZED_ERROR);
|
125 | } else {
|
126 | listeners.focus[0]((navigation) => navigation.dispatch(action));
|
127 | }
|
128 | },
|
129 | [listeners.focus]
|
130 | );
|
131 |
|
132 | const canGoBack = React.useCallback(() => {
|
133 | if (listeners.focus[0] == null) {
|
134 | return false;
|
135 | }
|
136 |
|
137 | const { result, handled } = listeners.focus[0]((navigation) =>
|
138 | navigation.canGoBack()
|
139 | );
|
140 |
|
141 | if (handled) {
|
142 | return result;
|
143 | } else {
|
144 | return false;
|
145 | }
|
146 | }, [listeners.focus]);
|
147 |
|
148 | const resetRoot = React.useCallback(
|
149 | (state?: PartialState<NavigationState> | NavigationState) => {
|
150 | const target = state?.key ?? keyedListeners.getState.root?.().key;
|
151 |
|
152 | if (target == null) {
|
153 | console.error(NOT_INITIALIZED_ERROR);
|
154 | } else {
|
155 | listeners.focus[0]((navigation) =>
|
156 | navigation.dispatch({
|
157 | ...CommonActions.reset(state),
|
158 | target,
|
159 | })
|
160 | );
|
161 | }
|
162 | },
|
163 | [keyedListeners.getState, listeners.focus]
|
164 | );
|
165 |
|
166 | const getRootState = React.useCallback(() => {
|
167 | return keyedListeners.getState.root?.();
|
168 | }, [keyedListeners.getState]);
|
169 |
|
170 | const getCurrentRoute = React.useCallback(() => {
|
171 | const state = getRootState();
|
172 |
|
173 | if (state == null) {
|
174 | return undefined;
|
175 | }
|
176 |
|
177 | const route = findFocusedRoute(state);
|
178 |
|
179 | return route as Route<string> | undefined;
|
180 | }, [getRootState]);
|
181 |
|
182 | const emitter = useEventEmitter<NavigationContainerEventMap>();
|
183 |
|
184 | const { addOptionsGetter, getCurrentOptions } = useOptionsGetters({});
|
185 |
|
186 | const navigation: NavigationContainerRef<ParamListBase> = React.useMemo(
|
187 | () => ({
|
188 | ...Object.keys(CommonActions).reduce<any>((acc, name) => {
|
189 | acc[name] = (...args: any[]) =>
|
190 |
|
191 | dispatch(CommonActions[name](...args));
|
192 | return acc;
|
193 | }, {}),
|
194 | ...emitter.create('root'),
|
195 | dispatch,
|
196 | resetRoot,
|
197 | isFocused: () => true,
|
198 | canGoBack,
|
199 | getParent: () => undefined,
|
200 | getState: () => stateRef.current,
|
201 | getRootState,
|
202 | getCurrentRoute,
|
203 | getCurrentOptions,
|
204 | isReady: () => listeners.focus[0] != null,
|
205 | }),
|
206 | [
|
207 | canGoBack,
|
208 | dispatch,
|
209 | emitter,
|
210 | getCurrentOptions,
|
211 | getCurrentRoute,
|
212 | getRootState,
|
213 | listeners.focus,
|
214 | resetRoot,
|
215 | ]
|
216 | );
|
217 |
|
218 | React.useImperativeHandle(ref, () => navigation, [navigation]);
|
219 |
|
220 | const onDispatchAction = React.useCallback(
|
221 | (action: NavigationAction, noop: boolean) => {
|
222 | emitter.emit({
|
223 | type: '__unsafe_action__',
|
224 | data: { action, noop, stack: stackRef.current },
|
225 | });
|
226 | },
|
227 | [emitter]
|
228 | );
|
229 |
|
230 | const lastEmittedOptionsRef = React.useRef<object | undefined>();
|
231 |
|
232 | const onOptionsChange = React.useCallback(
|
233 | (options: object) => {
|
234 | if (lastEmittedOptionsRef.current === options) {
|
235 | return;
|
236 | }
|
237 |
|
238 | lastEmittedOptionsRef.current = options;
|
239 |
|
240 | emitter.emit({
|
241 | type: 'options',
|
242 | data: { options },
|
243 | });
|
244 | },
|
245 | [emitter]
|
246 | );
|
247 |
|
248 | const stackRef = React.useRef<string | undefined>();
|
249 |
|
250 | const builderContext = React.useMemo(
|
251 | () => ({
|
252 | addListener,
|
253 | addKeyedListener,
|
254 | onDispatchAction,
|
255 | onOptionsChange,
|
256 | stackRef,
|
257 | }),
|
258 | [addListener, addKeyedListener, onDispatchAction, onOptionsChange]
|
259 | );
|
260 |
|
261 | const scheduleContext = React.useMemo(
|
262 | () => ({ scheduleUpdate, flushUpdates }),
|
263 | [scheduleUpdate, flushUpdates]
|
264 | );
|
265 |
|
266 | const isInitialRef = React.useRef(true);
|
267 |
|
268 | const getIsInitial = React.useCallback(() => isInitialRef.current, []);
|
269 |
|
270 | const context = React.useMemo(
|
271 | () => ({
|
272 | state,
|
273 | getState,
|
274 | setState,
|
275 | getKey,
|
276 | setKey,
|
277 | getIsInitial,
|
278 | addOptionsGetter,
|
279 | }),
|
280 | [
|
281 | state,
|
282 | getState,
|
283 | setState,
|
284 | getKey,
|
285 | setKey,
|
286 | getIsInitial,
|
287 | addOptionsGetter,
|
288 | ]
|
289 | );
|
290 |
|
291 | const onStateChangeRef = React.useRef(onStateChange);
|
292 | const stateRef = React.useRef(state);
|
293 |
|
294 | React.useEffect(() => {
|
295 | isInitialRef.current = false;
|
296 | onStateChangeRef.current = onStateChange;
|
297 | stateRef.current = state;
|
298 | });
|
299 |
|
300 | React.useEffect(() => {
|
301 | const hydratedState = getRootState();
|
302 |
|
303 | if (process.env.NODE_ENV !== 'production') {
|
304 | if (hydratedState !== undefined) {
|
305 | const serializableResult = checkSerializable(hydratedState);
|
306 |
|
307 | if (!serializableResult.serializable) {
|
308 | const { location, reason } = serializableResult;
|
309 |
|
310 | let path = '';
|
311 | let pointer: Record<any, any> = hydratedState;
|
312 | let params = false;
|
313 |
|
314 | for (let i = 0; i < location.length; i++) {
|
315 | const curr = location[i];
|
316 | const prev = location[i - 1];
|
317 |
|
318 | pointer = pointer[curr];
|
319 |
|
320 | if (!params && curr === 'state') {
|
321 | continue;
|
322 | } else if (!params && curr === 'routes') {
|
323 | if (path) {
|
324 | path += ' > ';
|
325 | }
|
326 | } else if (
|
327 | !params &&
|
328 | typeof curr === 'number' &&
|
329 | prev === 'routes'
|
330 | ) {
|
331 | path += pointer?.name;
|
332 | } else if (!params) {
|
333 | path += ` > ${curr}`;
|
334 | params = true;
|
335 | } else {
|
336 | if (typeof curr === 'number' || /^[0-9]+$/.test(curr)) {
|
337 | path += `[${curr}]`;
|
338 | } else if (/^[a-z$_]+$/i.test(curr)) {
|
339 | path += `.${curr}`;
|
340 | } else {
|
341 | path += `[${JSON.stringify(curr)}]`;
|
342 | }
|
343 | }
|
344 | }
|
345 |
|
346 | const message = `Non-serializable values were found in the navigation state. Check:\n\n${path} (${reason})\n\nThis can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https:
|
347 |
|
348 | if (!serializableWarnings.includes(message)) {
|
349 | serializableWarnings.push(message);
|
350 | console.warn(message);
|
351 | }
|
352 | }
|
353 |
|
354 | const duplicateRouteNamesResult =
|
355 | checkDuplicateRouteNames(hydratedState);
|
356 |
|
357 | if (duplicateRouteNamesResult.length) {
|
358 | const message = `Found screens with the same name nested inside one another. Check:\n${duplicateRouteNamesResult.map(
|
359 | (locations) => `\n${locations.join(', ')}`
|
360 | )}\n\nThis can cause confusing behavior during navigation. Consider using unique names for each screen instead.`;
|
361 |
|
362 | if (!duplicateNameWarnings.includes(message)) {
|
363 | duplicateNameWarnings.push(message);
|
364 | console.warn(message);
|
365 | }
|
366 | }
|
367 | }
|
368 | }
|
369 |
|
370 | emitter.emit({ type: 'state', data: { state } });
|
371 |
|
372 | if (!isFirstMountRef.current && onStateChangeRef.current) {
|
373 | onStateChangeRef.current(hydratedState);
|
374 | }
|
375 |
|
376 | isFirstMountRef.current = false;
|
377 | }, [getRootState, emitter, state]);
|
378 |
|
379 | const defaultOnUnhandledAction = React.useCallback(
|
380 | (action: NavigationAction) => {
|
381 | if (process.env.NODE_ENV === 'production') {
|
382 | return;
|
383 | }
|
384 |
|
385 | const payload: Record<string, any> | undefined = action.payload;
|
386 |
|
387 | let message = `The action '${action.type}'${
|
388 | payload ? ` with payload ${JSON.stringify(action.payload)}` : ''
|
389 | } was not handled by any navigator.`;
|
390 |
|
391 | switch (action.type) {
|
392 | case 'NAVIGATE':
|
393 | case 'PUSH':
|
394 | case 'REPLACE':
|
395 | case 'JUMP_TO':
|
396 | if (payload?.name) {
|
397 | message += `\n\nDo you have a screen named '${payload.name}'?\n\nIf you're trying to navigate to a screen in a nested navigator, see https:
|
398 | } else {
|
399 | message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https:
|
400 | }
|
401 |
|
402 | break;
|
403 | case 'GO_BACK':
|
404 | case 'POP':
|
405 | case 'POP_TO_TOP':
|
406 | message += `\n\nIs there any screen to go back to?`;
|
407 | break;
|
408 | case 'OPEN_DRAWER':
|
409 | case 'CLOSE_DRAWER':
|
410 | case 'TOGGLE_DRAWER':
|
411 | message += `\n\nIs your screen inside a Drawer navigator?`;
|
412 | break;
|
413 | }
|
414 |
|
415 | message += `\n\nThis is a development-only warning and won't be shown in production.`;
|
416 |
|
417 | console.error(message);
|
418 | },
|
419 | []
|
420 | );
|
421 |
|
422 | let element = (
|
423 | <NavigationContainerRefContext.Provider value={navigation}>
|
424 | <ScheduleUpdateContext.Provider value={scheduleContext}>
|
425 | <NavigationBuilderContext.Provider value={builderContext}>
|
426 | <NavigationStateContext.Provider value={context}>
|
427 | <UnhandledActionContext.Provider
|
428 | value={onUnhandledAction ?? defaultOnUnhandledAction}
|
429 | >
|
430 | <EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
431 | </UnhandledActionContext.Provider>
|
432 | </NavigationStateContext.Provider>
|
433 | </NavigationBuilderContext.Provider>
|
434 | </ScheduleUpdateContext.Provider>
|
435 | </NavigationContainerRefContext.Provider>
|
436 | );
|
437 |
|
438 | if (independent) {
|
439 | // We need to clear any existing contexts for nested independent container to work correctly
|
440 | element = (
|
441 | <NavigationRouteContext.Provider value={undefined}>
|
442 | <NavigationContext.Provider value={undefined}>
|
443 | {element}
|
444 | </NavigationContext.Provider>
|
445 | </NavigationRouteContext.Provider>
|
446 | );
|
447 | }
|
448 |
|
449 | return element;
|
450 | }
|
451 | );
|
452 |
|
453 | export default BaseNavigationContainer;
|
454 |
|
\ | No newline at end of file |