UNPKG

@react-navigation/core

Version:

Core utilities for building navigators

338 lines (335 loc) 13.3 kB
import { CommonActions } from '@react-navigation/routers'; import * as React from 'react'; import checkDuplicateRouteNames from './checkDuplicateRouteNames'; import checkSerializable from './checkSerializable'; import { NOT_INITIALIZED_ERROR } from './createNavigationContainerRef'; import EnsureSingleNavigator from './EnsureSingleNavigator'; import findFocusedRoute from './findFocusedRoute'; import NavigationBuilderContext from './NavigationBuilderContext'; import NavigationContainerRefContext from './NavigationContainerRefContext'; import NavigationContext from './NavigationContext'; import NavigationRouteContext from './NavigationRouteContext'; import NavigationStateContext from './NavigationStateContext'; import UnhandledActionContext from './UnhandledActionContext'; import useChildListeners from './useChildListeners'; import useEventEmitter from './useEventEmitter'; import useKeyedChildListeners from './useKeyedChildListeners'; import useOptionsGetters from './useOptionsGetters'; import { ScheduleUpdateContext } from './useScheduleUpdate'; import useSyncState from './useSyncState'; const serializableWarnings = []; const duplicateNameWarnings = []; /** * Remove `key` and `routeNames` from the state objects recursively to get partial state. * * @param state Initial state object. */ const getPartialState = state => { if (state === undefined) { return; } // eslint-disable-next-line @typescript-eslint/no-unused-vars const { key, routeNames, ...partialState } = state; return { ...partialState, stale: true, routes: state.routes.map(route => { if (route.state === undefined) { return route; } return { ...route, state: getPartialState(route.state) }; }) }; }; /** * Container component which holds the navigation state. * This should be rendered at the root wrapping the whole app. * * @param props.initialState Initial state object for the navigation tree. * @param props.onStateChange Callback which is called with the latest navigation state when it changes. * @param props.children Child elements to render the content. * @param props.ref Ref object which refers to the navigation object containing helper methods. */ const BaseNavigationContainer = /*#__PURE__*/React.forwardRef(function BaseNavigationContainer(_ref, ref) { let { initialState, onStateChange, onUnhandledAction, independent, children } = _ref; const parent = React.useContext(NavigationStateContext); if (!parent.isDefault && !independent) { throw new Error("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."); } const [state, getState, setState, scheduleUpdate, flushUpdates] = useSyncState(() => getPartialState(initialState == null ? undefined : initialState)); const isFirstMountRef = React.useRef(true); const navigatorKeyRef = React.useRef(); const getKey = React.useCallback(() => navigatorKeyRef.current, []); const setKey = React.useCallback(key => { navigatorKeyRef.current = key; }, []); const { listeners, addListener } = useChildListeners(); const { keyedListeners, addKeyedListener } = useKeyedChildListeners(); const dispatch = React.useCallback(action => { if (listeners.focus[0] == null) { console.error(NOT_INITIALIZED_ERROR); } else { listeners.focus[0](navigation => navigation.dispatch(action)); } }, [listeners.focus]); const canGoBack = React.useCallback(() => { if (listeners.focus[0] == null) { return false; } const { result, handled } = listeners.focus[0](navigation => navigation.canGoBack()); if (handled) { return result; } else { return false; } }, [listeners.focus]); const resetRoot = React.useCallback(state => { var _keyedListeners$getSt, _keyedListeners$getSt2; const target = (state === null || state === void 0 ? void 0 : state.key) ?? ((_keyedListeners$getSt = (_keyedListeners$getSt2 = keyedListeners.getState).root) === null || _keyedListeners$getSt === void 0 ? void 0 : _keyedListeners$getSt.call(_keyedListeners$getSt2).key); if (target == null) { console.error(NOT_INITIALIZED_ERROR); } else { listeners.focus[0](navigation => navigation.dispatch({ ...CommonActions.reset(state), target })); } }, [keyedListeners.getState, listeners.focus]); const getRootState = React.useCallback(() => { var _keyedListeners$getSt3, _keyedListeners$getSt4; return (_keyedListeners$getSt3 = (_keyedListeners$getSt4 = keyedListeners.getState).root) === null || _keyedListeners$getSt3 === void 0 ? void 0 : _keyedListeners$getSt3.call(_keyedListeners$getSt4); }, [keyedListeners.getState]); const getCurrentRoute = React.useCallback(() => { const state = getRootState(); if (state == null) { return undefined; } const route = findFocusedRoute(state); return route; }, [getRootState]); const emitter = useEventEmitter(); const { addOptionsGetter, getCurrentOptions } = useOptionsGetters({}); const navigation = React.useMemo(() => ({ ...Object.keys(CommonActions).reduce((acc, name) => { acc[name] = function () { return ( // @ts-expect-error: this is ok dispatch(CommonActions[name](...arguments)) ); }; return acc; }, {}), ...emitter.create('root'), dispatch, resetRoot, isFocused: () => true, canGoBack, getParent: () => undefined, getState: () => stateRef.current, getRootState, getCurrentRoute, getCurrentOptions, isReady: () => listeners.focus[0] != null, setOptions: () => { throw new Error('Cannot call setOptions outside a screen'); } }), [canGoBack, dispatch, emitter, getCurrentOptions, getCurrentRoute, getRootState, listeners.focus, resetRoot]); React.useImperativeHandle(ref, () => navigation, [navigation]); const onDispatchAction = React.useCallback((action, noop) => { emitter.emit({ type: '__unsafe_action__', data: { action, noop, stack: stackRef.current } }); }, [emitter]); const lastEmittedOptionsRef = React.useRef(); const onOptionsChange = React.useCallback(options => { if (lastEmittedOptionsRef.current === options) { return; } lastEmittedOptionsRef.current = options; emitter.emit({ type: 'options', data: { options } }); }, [emitter]); const stackRef = React.useRef(); const builderContext = React.useMemo(() => ({ addListener, addKeyedListener, onDispatchAction, onOptionsChange, stackRef }), [addListener, addKeyedListener, onDispatchAction, onOptionsChange]); const scheduleContext = React.useMemo(() => ({ scheduleUpdate, flushUpdates }), [scheduleUpdate, flushUpdates]); const isInitialRef = React.useRef(true); const getIsInitial = React.useCallback(() => isInitialRef.current, []); const context = React.useMemo(() => ({ state, getState, setState, getKey, setKey, getIsInitial, addOptionsGetter }), [state, getState, setState, getKey, setKey, getIsInitial, addOptionsGetter]); const onStateChangeRef = React.useRef(onStateChange); const stateRef = React.useRef(state); React.useEffect(() => { isInitialRef.current = false; onStateChangeRef.current = onStateChange; stateRef.current = state; }); React.useEffect(() => { const hydratedState = getRootState(); if (process.env.NODE_ENV !== 'production') { if (hydratedState !== undefined) { const serializableResult = checkSerializable(hydratedState); if (!serializableResult.serializable) { const { location, reason } = serializableResult; let path = ''; let pointer = hydratedState; let params = false; for (let i = 0; i < location.length; i++) { const curr = location[i]; const prev = location[i - 1]; pointer = pointer[curr]; if (!params && curr === 'state') { continue; } else if (!params && curr === 'routes') { if (path) { path += ' > '; } } else if (!params && typeof curr === 'number' && prev === 'routes') { var _pointer; path += (_pointer = pointer) === null || _pointer === void 0 ? void 0 : _pointer.name; } else if (!params) { path += ` > ${curr}`; params = true; } else { if (typeof curr === 'number' || /^[0-9]+$/.test(curr)) { path += `[${curr}]`; } else if (/^[a-z$_]+$/i.test(curr)) { path += `.${curr}`; } else { path += `[${JSON.stringify(curr)}]`; } } } 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.`; if (!serializableWarnings.includes(message)) { serializableWarnings.push(message); console.warn(message); } } const duplicateRouteNamesResult = checkDuplicateRouteNames(hydratedState); if (duplicateRouteNamesResult.length) { const message = `Found screens with the same name nested inside one another. Check:\n${duplicateRouteNamesResult.map(locations => `\n${locations.join(', ')}`)}\n\nThis can cause confusing behavior during navigation. Consider using unique names for each screen instead.`; if (!duplicateNameWarnings.includes(message)) { duplicateNameWarnings.push(message); console.warn(message); } } } } emitter.emit({ type: 'state', data: { state } }); if (!isFirstMountRef.current && onStateChangeRef.current) { onStateChangeRef.current(hydratedState); } isFirstMountRef.current = false; }, [getRootState, emitter, state]); const defaultOnUnhandledAction = React.useCallback(action => { if (process.env.NODE_ENV === 'production') { return; } const payload = action.payload; let message = `The action '${action.type}'${payload ? ` with payload ${JSON.stringify(action.payload)}` : ''} was not handled by any navigator.`; switch (action.type) { case 'NAVIGATE': case 'PUSH': case 'REPLACE': case 'JUMP_TO': if (payload !== null && payload !== void 0 && payload.name) { 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.`; } else { message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https://reactnavigation.org/docs/navigation-actions for usage.`; } break; case 'GO_BACK': case 'POP': case 'POP_TO_TOP': message += `\n\nIs there any screen to go back to?`; break; case 'OPEN_DRAWER': case 'CLOSE_DRAWER': case 'TOGGLE_DRAWER': message += `\n\nIs your screen inside a Drawer navigator?`; break; } message += `\n\nThis is a development-only warning and won't be shown in production.`; console.error(message); }, []); let element = /*#__PURE__*/React.createElement(NavigationContainerRefContext.Provider, { value: navigation }, /*#__PURE__*/React.createElement(ScheduleUpdateContext.Provider, { value: scheduleContext }, /*#__PURE__*/React.createElement(NavigationBuilderContext.Provider, { value: builderContext }, /*#__PURE__*/React.createElement(NavigationStateContext.Provider, { value: context }, /*#__PURE__*/React.createElement(UnhandledActionContext.Provider, { value: onUnhandledAction ?? defaultOnUnhandledAction }, /*#__PURE__*/React.createElement(EnsureSingleNavigator, null, children)))))); if (independent) { // We need to clear any existing contexts for nested independent container to work correctly element = /*#__PURE__*/React.createElement(NavigationRouteContext.Provider, { value: undefined }, /*#__PURE__*/React.createElement(NavigationContext.Provider, { value: undefined }, element)); } return element; }); export default BaseNavigationContainer; //# sourceMappingURL=BaseNavigationContainer.js.map