UNPKG

14.6 kBTypeScriptView Raw
1import {
2 CommonActions,
3 InitialState,
4 NavigationAction,
5 NavigationState,
6 ParamListBase,
7 PartialState,
8 Route,
9} from '@react-navigation/routers';
10import * as React from 'react';
11
12import checkDuplicateRouteNames from './checkDuplicateRouteNames';
13import checkSerializable from './checkSerializable';
14import { NOT_INITIALIZED_ERROR } from './createNavigationContainerRef';
15import EnsureSingleNavigator from './EnsureSingleNavigator';
16import findFocusedRoute from './findFocusedRoute';
17import NavigationBuilderContext from './NavigationBuilderContext';
18import NavigationContainerRefContext from './NavigationContainerRefContext';
19import NavigationContext from './NavigationContext';
20import NavigationRouteContext from './NavigationRouteContext';
21import NavigationStateContext from './NavigationStateContext';
22import type {
23 NavigationContainerEventMap,
24 NavigationContainerProps,
25 NavigationContainerRef,
26} from './types';
27import UnhandledActionContext from './UnhandledActionContext';
28import useChildListeners from './useChildListeners';
29import useEventEmitter from './useEventEmitter';
30import useKeyedChildListeners from './useKeyedChildListeners';
31import useOptionsGetters from './useOptionsGetters';
32import { ScheduleUpdateContext } from './useScheduleUpdate';
33import useSyncState from './useSyncState';
34
35type State = NavigationState | PartialState<NavigationState> | undefined;
36
37const serializableWarnings: string[] = [];
38const duplicateNameWarnings: string[] = [];
39
40/**
41 * Remove `key` and `routeNames` from the state objects recursively to get partial state.
42 *
43 * @param state Initial state object.
44 */
45const getPartialState = (
46 state: InitialState | undefined
47): PartialState<NavigationState> | undefined => {
48 if (state === undefined) {
49 return;
50 }
51
52 // eslint-disable-next-line @typescript-eslint/no-unused-vars
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 * Container component which holds the navigation state.
72 * This should be rendered at the root wrapping the whole app.
73 *
74 * @param props.initialState Initial state object for the navigation tree.
75 * @param props.onStateChange Callback which is called with the latest navigation state when it changes.
76 * @param props.children Child elements to render the content.
77 * @param props.ref Ref object which refers to the navigation object containing helper methods.
78 */
79const 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 // @ts-expect-error: this is ok
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://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details.`;
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://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.`;
398 } else {
399 message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https://reactnavigation.org/docs/navigation-actions for usage.`;
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
453export default BaseNavigationContainer;
454
\No newline at end of file