import type { EventMapBase, NavigationState, ParamListBase, RouteProp, ScreenListeners, } from '@react-navigation/native' import React from 'react' import { Route, useRouteNode, type DynamicConvention, type LoadedRoute, type RouteNode, } from './Route' import EXPO_ROUTER_IMPORT_MODE from './import-mode' import { Screen } from './primitives' import { sortRoutesWithInitial } from './sortRoutes' import { EmptyRoute } from './views/EmptyRoute' import { SuspenseFallback } from './views/SuspenseFallback' import { Try } from './views/Try' export type ScreenProps< TOptions extends Record = Record, State extends NavigationState = NavigationState, EventMap extends EventMapBase = EventMapBase, > = { /** Name is required when used inside a Layout component. */ name?: string /** * Redirect to the nearest sibling route. * If all children are redirect={true}, the layout will render `null` as there are no children to render. */ redirect?: boolean initialParams?: Record options?: TOptions listeners?: | ScreenListeners | ((prop: { route: RouteProp navigation: any }) => ScreenListeners) getId?: ({ params }: { params?: Record }) => string | undefined } function getSortedChildren( children: RouteNode[], order?: ScreenProps[], initialRouteName?: string ): { route: RouteNode; props: Partial }[] { if (!order?.length) { return children .sort(sortRoutesWithInitial(initialRouteName)) .map((route) => ({ route, props: {} })) } const entries = [...children] const ordered = order .map(({ name, redirect, initialParams, listeners, options, getId }) => { if (!entries.length) { console.warn(`[Layout children]: Too many screens defined. Route "${name}" is extraneous.`) return null } const matchIndex = entries.findIndex((child) => child.route === name) if (matchIndex === -1) { console.warn( `[Layout children]: No route named "${name}" exists in nested children:`, children.map(({ route }) => route) ) return null } // Get match and remove from entries const match = entries[matchIndex] entries.splice(matchIndex, 1) // Ensure to return null after removing from entries. if (redirect) { if (typeof redirect === 'string') { throw new Error(`Redirecting to a specific route is not supported yet.`) } return null } return { route: match, props: { initialParams, listeners, options, getId }, } }) .filter(Boolean) as { route: RouteNode props: Partial }[] // Add any remaining children ordered.push( ...entries.sort(sortRoutesWithInitial(initialRouteName)).map((route) => ({ route, props: {} })) ) return ordered } /** * @returns React Navigation screens sorted by the `route` property. */ export function useSortedScreens(order: ScreenProps[]): React.ReactNode[] { const node = useRouteNode() return React.useMemo(() => { const sorted = node?.children?.length ? getSortedChildren(node.children, order, node.initialRouteName) : [] return sorted.map((value) => routeToScreen(value.route, value.props)) }, [node?.children, node?.initialRouteName, order]) } function fromImport({ ErrorBoundary, ...component }: LoadedRoute) { if (ErrorBoundary) { return { default: React.forwardRef((props: any, ref: any) => { const children = React.createElement(component.default || EmptyRoute, { ...props, ref, }) return {children} }), } } if (process.env.NODE_ENV !== 'production') { if ( typeof component.default === 'object' && component.default && Object.keys(component.default).length === 0 ) { return { default: EmptyRoute } } } return { default: component.default } } function fromLoadedRoute(res: LoadedRoute) { if (!(res instanceof Promise)) { return fromImport(res) } return res.then(fromImport) } // TODO: Maybe there's a more React-y way to do this? // Without this store, the process enters a recursive loop. const qualifiedStore = new WeakMap>() /** Wrap the component with various enhancements and add access to child routes. */ export function getQualifiedRouteComponent(value: RouteNode) { if (qualifiedStore.has(value)) { return qualifiedStore.get(value)! } let ScreenComponent: React.ForwardRefExoticComponent> // TODO: This ensures sync doesn't use React.lazy, but it's not ideal. if (EXPO_ROUTER_IMPORT_MODE === 'lazy') { ScreenComponent = React.lazy(async () => { const res = value.loadRoute() return fromLoadedRoute(res) as Promise<{ default: React.ComponentType }> }) } else { ScreenComponent = React.forwardRef((props, ref) => { const res = value.loadRoute() const Component = fromImport(res).default as React.ComponentType return }) } const getLoadable = (props: any, ref: any) => { return ( }> ) } const QualifiedRoute = React.forwardRef( ( { // Remove these React Navigation props to // enforce usage of router hooks (where the query params are correct). route, navigation, // Pass all other props to the component ...props }: any, ref: any ) => { const loadable = getLoadable(props, ref) return {loadable} } ) QualifiedRoute.displayName = `Route(${value.route})` qualifiedStore.set(value, QualifiedRoute) return QualifiedRoute } /** @returns a function which provides a screen id that matches the dynamic route name in params. */ export function createGetIdForRoute( route: Pick ) { const include = new Map() if (route.dynamic) { for (const segment of route.dynamic) { include.set(segment.name, segment) } } return ({ params = {} } = {} as { params?: Record }) => { const segments: string[] = [] for (const dynamic of include.values()) { const value = params?.[dynamic.name] if (Array.isArray(value) && value.length > 0) { // If we are an array with a value segments.push(value.join('/')) } else if (value && !Array.isArray(value)) { // If we have a value and not an empty array segments.push(value) } else if (dynamic.deep) { segments.push(`[...${dynamic.name}]`) } else { segments.push(`[${dynamic.name}]`) } } return segments.join('/') ?? route.contextKey } } function routeToScreen(route: RouteNode, { options, ...props }: Partial = {}) { return ( { // Only eager load generated components const staticOptions = route.generated ? route.loadRoute()?.getNavOptions : null const staticResult = typeof staticOptions === 'function' ? staticOptions(args) : staticOptions const dynamicResult = typeof options === 'function' ? options?.(args) : options const output = { ...staticResult, ...dynamicResult, } // Prevent generated screens from showing up in the tab bar. if (route.generated) { output.tabBarButton = () => null // TODO: React Navigation doesn't provide a way to prevent rendering the drawer item. output.drawerItemStyle = { height: 0, display: 'none' } } return output }} getComponent={() => getQualifiedRouteComponent(route)} /> ) }