diff --git a/packages/core/src/SceneView.tsx b/packages/core/src/SceneView.tsx index df8cabdf..6e1e0363 100644 --- a/packages/core/src/SceneView.tsx +++ b/packages/core/src/SceneView.tsx @@ -18,9 +18,8 @@ type Props< > = { screen: RouteConfig; navigation: NavigationProp; - route: Route & { - state?: NavigationState | PartialState; - }; + route: Route; + routeState: NavigationState | PartialState | undefined; getState: () => State; setState: (state: State) => void; options: object; @@ -38,6 +37,7 @@ export default function SceneView< screen, route, navigation, + routeState, getState, setState, options, @@ -86,7 +86,7 @@ export default function SceneView< const context = React.useMemo( () => ({ - state: route.state, + state: routeState, getState: getCurrentState, setState: setCurrentState, getKey, @@ -95,7 +95,7 @@ export default function SceneView< addOptionsGetter, }), [ - route.state, + routeState, getCurrentState, setCurrentState, getKey, diff --git a/packages/core/src/getFocusedRouteNameFromRoute.tsx b/packages/core/src/getFocusedRouteNameFromRoute.tsx index db783822..8fc661b6 100644 --- a/packages/core/src/getFocusedRouteNameFromRoute.tsx +++ b/packages/core/src/getFocusedRouteNameFromRoute.tsx @@ -3,11 +3,17 @@ import type { PartialState, NavigationState, } from '@react-navigation/routers'; +import { SUPPRESS_STATE_ACCESS_WARNING } from './useRouteCache'; export default function getFocusedRouteNameFromRoute( route: Partial> & { state?: PartialState } ): string | undefined { + SUPPRESS_STATE_ACCESS_WARNING.value = true; + const state = route.state; + + SUPPRESS_STATE_ACCESS_WARNING.value = false; + const params = route.params as { screen?: unknown } | undefined; const routeName = state diff --git a/packages/core/src/useDescriptors.tsx b/packages/core/src/useDescriptors.tsx index bad3b32d..e41dfe46 100644 --- a/packages/core/src/useDescriptors.tsx +++ b/packages/core/src/useDescriptors.tsx @@ -12,6 +12,7 @@ import NavigationBuilderContext, { } from './NavigationBuilderContext'; import type { NavigationEventEmitter } from './useEventEmitter'; import useNavigationCache from './useNavigationCache'; +import useRouteCache from './useRouteCache'; import NavigationContext from './NavigationContext'; import NavigationRouteContext from './NavigationRouteContext'; import type { @@ -113,9 +114,11 @@ export default function useDescriptors< emitter, }); - return state.routes.reduce< + const routes = useRouteCache(state.routes); + + return routes.reduce< Record> - >((acc, route) => { + >((acc, route, i) => { const screen = screens[route.name]; const navigation = navigations[route.key]; @@ -151,6 +154,7 @@ export default function useDescriptors< navigation={navigation} route={route} screen={screen} + routeState={state.routes[i].state} getState={getState} setState={setState} options={routeOptions} diff --git a/packages/core/src/useRouteCache.tsx b/packages/core/src/useRouteCache.tsx new file mode 100644 index 00000000..1dfd23dd --- /dev/null +++ b/packages/core/src/useRouteCache.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import type { + ParamListBase, + NavigationState, + Route, +} from '@react-navigation/routers'; +import type { RouteProp } from './types'; + +type RouteCache = Map, RouteProp>; + +/** + * Utilites such as `getFocusedRouteNameFromRoute` need to access state. + * So we need a way to suppress the warning for those use cases. + * This is fine since they are internal utilities and this is not public API. + */ +export const SUPPRESS_STATE_ACCESS_WARNING = { value: false }; + +/** + * Hook to cache route props for each screen in the navigator. + * This lets add warnings and modifications to the route object but keep references between renders. + */ +export default function useRouteCache( + routes: State['routes'] +) { + // Cache object which holds route objects for each screen + const cache = React.useMemo(() => ({ current: new Map() as RouteCache }), []); + + if (process.env.NODE_ENV === 'production') { + // We don't want the overhead of creating extra maps every render in prod + return routes; + } + + cache.current = routes.reduce((acc, route) => { + const previous = cache.current.get(route); + + if (previous) { + // If a cached route object already exists, reuse it + acc.set(route, previous); + } else { + const proxy = { ...route }; + + Object.defineProperty(proxy, 'state', { + get() { + if (!SUPPRESS_STATE_ACCESS_WARNING.value) { + console.warn( + "Accessing the 'state' property of the 'route' object is not supported. If you want to get the focused route name, use the 'getFocusedRouteNameFromRoute' helper instead: https://reactnavigation.org/docs/screen-options-resolution/#setting-parent-screen-options-based-on-child-navigators-state" + ); + } + + return route.state; + }, + }); + + acc.set(route, proxy); + } + + return acc; + }, new Map() as RouteCache); + + return Array.from(cache.current.values()); +}