UNPKG

@react-navigation/core

Version:

Core utilities for building navigators

457 lines (448 loc) 23.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = useNavigationBuilder; var _routers = require("@react-navigation/routers"); var React = _interopRequireWildcard(require("react")); var _reactIs = require("react-is"); var _Group = _interopRequireDefault(require("./Group")); var _isArrayEqual = _interopRequireDefault(require("./isArrayEqual")); var _isRecordEqual = _interopRequireDefault(require("./isRecordEqual")); var _NavigationHelpersContext = _interopRequireDefault(require("./NavigationHelpersContext")); var _NavigationRouteContext = _interopRequireDefault(require("./NavigationRouteContext")); var _NavigationStateContext = _interopRequireDefault(require("./NavigationStateContext")); var _PreventRemoveProvider = _interopRequireDefault(require("./PreventRemoveProvider")); var _Screen = _interopRequireDefault(require("./Screen")); var _types = require("./types"); var _useChildListeners = _interopRequireDefault(require("./useChildListeners")); var _useComponent = _interopRequireDefault(require("./useComponent")); var _useCurrentRender = _interopRequireDefault(require("./useCurrentRender")); var _useDescriptors = _interopRequireDefault(require("./useDescriptors")); var _useEventEmitter = _interopRequireDefault(require("./useEventEmitter")); var _useFocusedListenersChildrenAdapter = _interopRequireDefault(require("./useFocusedListenersChildrenAdapter")); var _useFocusEvents = _interopRequireDefault(require("./useFocusEvents")); var _useKeyedChildListeners = _interopRequireDefault(require("./useKeyedChildListeners")); var _useNavigationHelpers = _interopRequireDefault(require("./useNavigationHelpers")); var _useOnAction = _interopRequireDefault(require("./useOnAction")); var _useOnGetState = _interopRequireDefault(require("./useOnGetState")); var _useOnRouteFocus = _interopRequireDefault(require("./useOnRouteFocus")); var _useRegisterNavigator = _interopRequireDefault(require("./useRegisterNavigator")); var _useScheduleUpdate = _interopRequireDefault(require("./useScheduleUpdate")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } // This is to make TypeScript compiler happy // eslint-disable-next-line babel/no-unused-expressions _types.PrivateValueStore; const isValidKey = key => key === undefined || typeof key === 'string' && key !== ''; /** * Extract route config object from React children elements. * * @param children React Elements to extract the config from. */ const getRouteConfigsFromChildren = (children, groupKey, groupOptions) => { const configs = React.Children.toArray(children).reduce((acc, child) => { var _child$type, _child$props; if ( /*#__PURE__*/React.isValidElement(child)) { if (child.type === _Screen.default) { // We can only extract the config from `Screen` elements // If something else was rendered, it's probably a bug if (!isValidKey(child.props.navigationKey)) { throw new Error(`Got an invalid 'navigationKey' prop (${JSON.stringify(child.props.navigationKey)}) for the screen '${child.props.name}'. It must be a non-empty string or 'undefined'.`); } acc.push({ keys: [groupKey, child.props.navigationKey], options: groupOptions, props: child.props }); return acc; } if (child.type === React.Fragment || child.type === _Group.default) { if (!isValidKey(child.props.navigationKey)) { throw new Error(`Got an invalid 'navigationKey' prop (${JSON.stringify(child.props.navigationKey)}) for the group. It must be a non-empty string or 'undefined'.`); } // When we encounter a fragment or group, we need to dive into its children to extract the configs // This is handy to conditionally define a group of screens acc.push(...getRouteConfigsFromChildren(child.props.children, child.props.navigationKey, child.type !== _Group.default ? groupOptions : groupOptions != null ? [...groupOptions, child.props.screenOptions] : [child.props.screenOptions])); return acc; } } throw new Error(`A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found ${/*#__PURE__*/React.isValidElement(child) ? `'${typeof child.type === 'string' ? child.type : (_child$type = child.type) === null || _child$type === void 0 ? void 0 : _child$type.name}'${child.props != null && typeof child.props === 'object' && 'name' in child.props && (_child$props = child.props) !== null && _child$props !== void 0 && _child$props.name ? ` for the screen '${child.props.name}'` : ''}` : typeof child === 'object' ? JSON.stringify(child) : `'${String(child)}'`}). To render this component in the navigator, pass it in the 'component' prop to 'Screen'.`); }, []); if (process.env.NODE_ENV !== 'production') { configs.forEach(config => { const { name, children, component, getComponent } = config.props; if (typeof name !== 'string' || !name) { throw new Error(`Got an invalid name (${JSON.stringify(name)}) for the screen. It must be a non-empty string.`); } if (children != null || component !== undefined || getComponent !== undefined) { if (children != null && component !== undefined) { throw new Error(`Got both 'component' and 'children' props for the screen '${name}'. You must pass only one of them.`); } if (children != null && getComponent !== undefined) { throw new Error(`Got both 'getComponent' and 'children' props for the screen '${name}'. You must pass only one of them.`); } if (component !== undefined && getComponent !== undefined) { throw new Error(`Got both 'component' and 'getComponent' props for the screen '${name}'. You must pass only one of them.`); } if (children != null && typeof children !== 'function') { throw new Error(`Got an invalid value for 'children' prop for the screen '${name}'. It must be a function returning a React Element.`); } if (component !== undefined && !(0, _reactIs.isValidElementType)(component)) { throw new Error(`Got an invalid value for 'component' prop for the screen '${name}'. It must be a valid React Component.`); } if (getComponent !== undefined && typeof getComponent !== 'function') { throw new Error(`Got an invalid value for 'getComponent' prop for the screen '${name}'. It must be a function returning a React Component.`); } if (typeof component === 'function') { if (component.name === 'component') { // Inline anonymous functions passed in the `component` prop will have the name of the prop // It's relatively safe to assume that it's not a component since it should also have PascalCase name // We won't catch all scenarios here, but this should catch a good chunk of incorrect use. console.warn(`Looks like you're passing an inline function for 'component' prop for the screen '${name}' (e.g. component={() => <SomeComponent />}). Passing an inline function will cause the component state to be lost on re-render and cause perf issues since it's re-created every render. You can pass the function as children to 'Screen' instead to achieve the desired behaviour.`); } else if (/^[a-z]/.test(component.name)) { console.warn(`Got a component with the name '${component.name}' for the screen '${name}'. React Components must start with an uppercase letter. If you're passing a regular function and not a component, pass it as children to 'Screen' instead. Otherwise capitalize your component's name.`); } } } else { throw new Error(`Couldn't find a 'component', 'getComponent' or 'children' prop for the screen '${name}'. This can happen if you passed 'undefined'. You likely forgot to export your component from the file it's defined in, or mixed up default import and named import when importing.`); } }); } return configs; }; /** * Hook for building navigators. * * @param createRouter Factory method which returns router object. * @param options Options object containing `children` and additional options for the router. * @returns An object containing `state`, `navigation`, `descriptors` objects. */ function useNavigationBuilder(createRouter, options) { const navigatorKey = (0, _useRegisterNavigator.default)(); const route = React.useContext(_NavigationRouteContext.default); const { children, screenListeners, ...rest } = options; const { current: router } = React.useRef(createRouter({ ...rest, ...(route !== null && route !== void 0 && route.params && route.params.state == null && route.params.initial !== false && typeof route.params.screen === 'string' ? { initialRouteName: route.params.screen } : null) })); const routeConfigs = getRouteConfigsFromChildren(children); const screens = routeConfigs.reduce((acc, config) => { if (config.props.name in acc) { throw new Error(`A navigator cannot contain multiple 'Screen' components with the same name (found duplicate screen named '${config.props.name}')`); } acc[config.props.name] = config; return acc; }, {}); const routeNames = routeConfigs.map(config => config.props.name); const routeKeyList = routeNames.reduce((acc, curr) => { acc[curr] = screens[curr].keys.map(key => key ?? '').join(':'); return acc; }, {}); const routeParamList = routeNames.reduce((acc, curr) => { const { initialParams } = screens[curr].props; acc[curr] = initialParams; return acc; }, {}); const routeGetIdList = routeNames.reduce((acc, curr) => Object.assign(acc, { [curr]: screens[curr].props.getId }), {}); if (!routeNames.length) { throw new Error("Couldn't find any screens for the navigator. Have you defined any screens as its children?"); } const isStateValid = React.useCallback(state => state.type === undefined || state.type === router.type, [router.type]); const isStateInitialized = React.useCallback(state => state !== undefined && state.stale === false && isStateValid(state), [isStateValid]); const { state: currentState, getState: getCurrentState, setState: setCurrentState, setKey, getKey, getIsInitial } = React.useContext(_NavigationStateContext.default); const stateCleanedUp = React.useRef(false); const cleanUpState = React.useCallback(() => { setCurrentState(undefined); stateCleanedUp.current = true; }, [setCurrentState]); const setState = React.useCallback(state => { if (stateCleanedUp.current) { // State might have been already cleaned up due to unmount // We do not want to expose API allowing to override this // This would lead to old data preservation on main navigator unmount return; } setCurrentState(state); }, [setCurrentState]); const [initializedState, isFirstStateInitialization] = React.useMemo(() => { var _route$params4; const initialRouteParamList = routeNames.reduce((acc, curr) => { var _route$params, _route$params2, _route$params3; const { initialParams } = screens[curr].props; const initialParamsFromParams = (route === null || route === void 0 ? void 0 : (_route$params = route.params) === null || _route$params === void 0 ? void 0 : _route$params.state) == null && (route === null || route === void 0 ? void 0 : (_route$params2 = route.params) === null || _route$params2 === void 0 ? void 0 : _route$params2.initial) !== false && (route === null || route === void 0 ? void 0 : (_route$params3 = route.params) === null || _route$params3 === void 0 ? void 0 : _route$params3.screen) === curr ? route.params.params : undefined; acc[curr] = initialParams !== undefined || initialParamsFromParams !== undefined ? { ...initialParams, ...initialParamsFromParams } : undefined; return acc; }, {}); // If the current state isn't initialized on first render, we initialize it // We also need to re-initialize it if the state passed from parent was changed (maybe due to reset) // Otherwise assume that the state was provided as initial state // So we need to rehydrate it to make it usable if ((currentState === undefined || !isStateValid(currentState)) && (route === null || route === void 0 ? void 0 : (_route$params4 = route.params) === null || _route$params4 === void 0 ? void 0 : _route$params4.state) == null) { return [router.getInitialState({ routeNames, routeParamList: initialRouteParamList, routeGetIdList }), true]; } else { var _route$params5; return [router.getRehydratedState((route === null || route === void 0 ? void 0 : (_route$params5 = route.params) === null || _route$params5 === void 0 ? void 0 : _route$params5.state) ?? currentState, { routeNames, routeParamList: initialRouteParamList, routeGetIdList }), false]; } // We explicitly don't include routeNames, route.params etc. in the dep list // below. We want to avoid forcing a new state to be calculated in those cases // Instead, we handle changes to these in the nextState code below. Note // that some changes to routeConfigs are explicitly ignored, such as changes // to initialParams // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentState, router, isStateValid]); const previousRouteKeyListRef = React.useRef(routeKeyList); React.useEffect(() => { previousRouteKeyListRef.current = routeKeyList; }); const previousRouteKeyList = previousRouteKeyListRef.current; let state = // If the state isn't initialized, or stale, use the state we initialized instead // The state won't update until there's a change needed in the state we have initalized locally // So it'll be `undefined` or stale until the first navigation event happens isStateInitialized(currentState) ? currentState : initializedState; let nextState = state; if (!(0, _isArrayEqual.default)(state.routeNames, routeNames) || !(0, _isRecordEqual.default)(routeKeyList, previousRouteKeyList)) { // When the list of route names change, the router should handle it to remove invalid routes nextState = router.getStateForRouteNamesChange(state, { routeNames, routeParamList, routeGetIdList, routeKeyChanges: Object.keys(routeKeyList).filter(name => previousRouteKeyList.hasOwnProperty(name) && routeKeyList[name] !== previousRouteKeyList[name]) }); } const previousNestedParamsRef = React.useRef(route === null || route === void 0 ? void 0 : route.params); React.useEffect(() => { previousNestedParamsRef.current = route === null || route === void 0 ? void 0 : route.params; }, [route === null || route === void 0 ? void 0 : route.params]); if (route !== null && route !== void 0 && route.params) { const previousParams = previousNestedParamsRef.current; let action; if (typeof route.params.state === 'object' && route.params.state != null && route.params !== previousParams) { // If the route was updated with new state, we should reset to it action = _routers.CommonActions.reset(route.params.state); } else if (typeof route.params.screen === 'string' && (route.params.initial === false && isFirstStateInitialization || route.params !== previousParams)) { // If the route was updated with new screen name and/or params, we should navigate there action = _routers.CommonActions.navigate({ name: route.params.screen, params: route.params.params, path: route.params.path }); } // The update should be limited to current navigator only, so we call the router manually const updatedState = action ? router.getStateForAction(nextState, action, { routeNames, routeParamList, routeGetIdList }) : null; nextState = updatedState !== null ? router.getRehydratedState(updatedState, { routeNames, routeParamList, routeGetIdList }) : nextState; } const shouldUpdate = state !== nextState; (0, _useScheduleUpdate.default)(() => { if (shouldUpdate) { // If the state needs to be updated, we'll schedule an update setState(nextState); } }); // The up-to-date state will come in next render, but we don't need to wait for it // We can't use the outdated state since the screens have changed, which will cause error due to mismatched config // So we override the state object we return to use the latest state as soon as possible state = nextState; React.useEffect(() => { setKey(navigatorKey); if (!getIsInitial()) { // If it's not initial render, we need to update the state // This will make sure that our container gets notifier of state changes due to new mounts // This is necessary for proper screen tracking, URL updates etc. setState(nextState); } return () => { // We need to clean up state for this navigator on unmount // We do it in a timeout because we need to detect if another navigator mounted in the meantime // For example, if another navigator has started rendering, we should skip cleanup // Otherwise, our cleanup step will cleanup state for the other navigator and re-initialize it setTimeout(() => { if (getCurrentState() !== undefined && getKey() === navigatorKey) { cleanUpState(); } }, 0); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // We initialize this ref here to avoid a new getState getting initialized // whenever initializedState changes. We want getState to have access to the // latest initializedState, but don't need it to change when that happens const initializedStateRef = React.useRef(); initializedStateRef.current = initializedState; const getState = React.useCallback(() => { const currentState = getCurrentState(); return isStateInitialized(currentState) ? currentState : initializedStateRef.current; }, [getCurrentState, isStateInitialized]); const emitter = (0, _useEventEmitter.default)(e => { let routeNames = []; let route; if (e.target) { var _route; route = state.routes.find(route => route.key === e.target); if ((_route = route) !== null && _route !== void 0 && _route.name) { routeNames.push(route.name); } } else { route = state.routes[state.index]; routeNames.push(...Object.keys(screens).filter(name => { var _route2; return ((_route2 = route) === null || _route2 === void 0 ? void 0 : _route2.name) === name; })); } if (route == null) { return; } const navigation = descriptors[route.key].navigation; const listeners = [].concat( // Get an array of listeners for all screens + common listeners on navigator ...[screenListeners, ...routeNames.map(name => { const { listeners } = screens[name].props; return listeners; })].map(listeners => { const map = typeof listeners === 'function' ? listeners({ route: route, navigation }) : listeners; return map ? Object.keys(map).filter(type => type === e.type).map(type => map === null || map === void 0 ? void 0 : map[type]) : undefined; })) // We don't want same listener to be called multiple times for same event // So we remove any duplicate functions from the array .filter((cb, i, self) => cb && self.lastIndexOf(cb) === i); listeners.forEach(listener => listener === null || listener === void 0 ? void 0 : listener(e)); }); (0, _useFocusEvents.default)({ state, emitter }); React.useEffect(() => { emitter.emit({ type: 'state', data: { state } }); }, [emitter, state]); const { listeners: childListeners, addListener } = (0, _useChildListeners.default)(); const { keyedListeners, addKeyedListener } = (0, _useKeyedChildListeners.default)(); const onAction = (0, _useOnAction.default)({ router, getState, setState, key: route === null || route === void 0 ? void 0 : route.key, actionListeners: childListeners.action, beforeRemoveListeners: keyedListeners.beforeRemove, routerConfigOptions: { routeNames, routeParamList, routeGetIdList }, emitter }); const onRouteFocus = (0, _useOnRouteFocus.default)({ router, key: route === null || route === void 0 ? void 0 : route.key, getState, setState }); const navigation = (0, _useNavigationHelpers.default)({ id: options.id, onAction, getState, emitter, router }); (0, _useFocusedListenersChildrenAdapter.default)({ navigation, focusedListeners: childListeners.focus }); (0, _useOnGetState.default)({ getState, getStateListeners: keyedListeners.getState }); const descriptors = (0, _useDescriptors.default)({ state, screens, navigation, screenOptions: options.screenOptions, defaultScreenOptions: options.defaultScreenOptions, onAction, getState, setState, onRouteFocus, addListener, addKeyedListener, router, // @ts-expect-error: this should have both core and custom events, but too much work right now emitter }); (0, _useCurrentRender.default)({ state, navigation, descriptors }); const NavigationContent = (0, _useComponent.default)(children => /*#__PURE__*/React.createElement(_NavigationHelpersContext.default.Provider, { value: navigation }, /*#__PURE__*/React.createElement(_PreventRemoveProvider.default, null, children))); return { state, navigation, descriptors, NavigationContent }; } //# sourceMappingURL=useNavigationBuilder.js.map