From 97a5844cd5ad28d592423a9ee8dd346d70a5cb51 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 23 Feb 2026 15:03:23 +0100 Subject: [PATCH 01/27] reusing navigators --- .../GetStateForActionHandlers.ts | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index b9a8ad120b0c..a606c394821c 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -120,17 +120,40 @@ function handlePushFullscreenAction( configOptions: RouterConfigOptions, stackRouter: Router, CommonActions.Action | StackActionType>, ) { - const targetScreen = action.payload?.params && 'screen' in action.payload.params ? (action.payload?.params?.screen as string) : undefined; const navigatorName = action.payload.name; + const params = action.payload.params; + + const targetScreen = params && 'screen' in params ? (params.screen as string) : undefined; + + const existingRoute = state.routes.find((r) => r.name === navigatorName); + + if (existingRoute) { + if (existingRoute.key && targetScreen && !SCREENS_WITH_NAVIGATION_TAB_BAR.includes(targetScreen)) { + screensWithEnteringAnimation.add(existingRoute.key); + } + + const updatedRoute = { + ...existingRoute, + params: {...existingRoute.params, ...params}, + }; + + const otherRoutes = state.routes.filter((r) => r.key !== existingRoute.key); + + return { + ...state, + routes: [...otherRoutes, updatedRoute], + index: otherRoutes.length, + }; + } - // If we navigate to the central screen of the split navigator, we need to filter this navigator from preloadedRoutes to remove a sidebar screen from the state const shouldFilterPreloadedRoutes = getIsNarrowLayout() && isSplitNavigatorName(navigatorName) && targetScreen !== SPLIT_TO_SIDEBAR[navigatorName] && state.preloadedRoutes?.some((preloadedRoute) => preloadedRoute.name === navigatorName); - const adjustedState = shouldFilterPreloadedRoutes ? {...state, preloadedRoutes: state.preloadedRoutes.filter((preloadedRoute) => preloadedRoute.name !== navigatorName)} : state; + const adjustedState = shouldFilterPreloadedRoutes ? {...state, preloadedRoutes: state.preloadedRoutes.filter((r) => r.name !== navigatorName)} : state; + const stateWithNavigator = stackRouter.getStateForAction(adjustedState, action, configOptions); if (!stateWithNavigator) { @@ -140,7 +163,6 @@ function handlePushFullscreenAction( const lastFullScreenRoute = stateWithNavigator.routes.at(-1); - // Transitioning to all central screens in each split should be animated if (lastFullScreenRoute?.key && targetScreen && !SCREENS_WITH_NAVIGATION_TAB_BAR.includes(targetScreen)) { screensWithEnteringAnimation.add(lastFullScreenRoute.key); } From 752fba855f9507130ced6dbe59d7a7d992592eab Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 23 Feb 2026 16:20:13 +0100 Subject: [PATCH 02/27] clean code --- .../createRootStackNavigator/GetStateForActionHandlers.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index a606c394821c..c82fd26bf41a 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -125,7 +125,7 @@ function handlePushFullscreenAction( const targetScreen = params && 'screen' in params ? (params.screen as string) : undefined; - const existingRoute = state.routes.find((r) => r.name === navigatorName); + const existingRoute = state.routes.find((route) => route.name === navigatorName); if (existingRoute) { if (existingRoute.key && targetScreen && !SCREENS_WITH_NAVIGATION_TAB_BAR.includes(targetScreen)) { @@ -137,7 +137,7 @@ function handlePushFullscreenAction( params: {...existingRoute.params, ...params}, }; - const otherRoutes = state.routes.filter((r) => r.key !== existingRoute.key); + const otherRoutes = state.routes.filter((route) => route.key !== existingRoute.key); return { ...state, @@ -146,13 +146,14 @@ function handlePushFullscreenAction( }; } + // If we navigate to the central screen of the split navigator, we need to filter this navigator from preloadedRoutes to remove a sidebar screen from the state const shouldFilterPreloadedRoutes = getIsNarrowLayout() && isSplitNavigatorName(navigatorName) && targetScreen !== SPLIT_TO_SIDEBAR[navigatorName] && state.preloadedRoutes?.some((preloadedRoute) => preloadedRoute.name === navigatorName); - const adjustedState = shouldFilterPreloadedRoutes ? {...state, preloadedRoutes: state.preloadedRoutes.filter((r) => r.name !== navigatorName)} : state; + const adjustedState = shouldFilterPreloadedRoutes ? {...state, preloadedRoutes: state.preloadedRoutes.filter((preloadedRoute) => preloadedRoute.name !== navigatorName)} : state; const stateWithNavigator = stackRouter.getStateForAction(adjustedState, action, configOptions); @@ -163,6 +164,7 @@ function handlePushFullscreenAction( const lastFullScreenRoute = stateWithNavigator.routes.at(-1); + // Transitioning to all central screens in each split should be animated if (lastFullScreenRoute?.key && targetScreen && !SCREENS_WITH_NAVIGATION_TAB_BAR.includes(targetScreen)) { screensWithEnteringAnimation.add(lastFullScreenRoute.key); } From 5df907fa760fcc1ce19c73b7beae96688a1acdbe Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 23 Feb 2026 16:28:25 +0100 Subject: [PATCH 03/27] fix browser history --- .../createRootStackNavigator/GetStateForActionHandlers.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index c82fd26bf41a..e3147ee0b1c1 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -137,12 +137,10 @@ function handlePushFullscreenAction( params: {...existingRoute.params, ...params}, }; - const otherRoutes = state.routes.filter((route) => route.key !== existingRoute.key); - return { ...state, - routes: [...otherRoutes, updatedRoute], - index: otherRoutes.length, + routes: [state.routes, updatedRoute], + index: state.routes.length, }; } From f8a61f00e101bfd2b6fa23b6d22b338cc109d01d Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Tue, 24 Feb 2026 11:32:57 +0100 Subject: [PATCH 04/27] fix ios undefined state issue --- .../createRootStackNavigator/GetStateForActionHandlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index e3147ee0b1c1..2e177c187566 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -139,7 +139,7 @@ function handlePushFullscreenAction( return { ...state, - routes: [state.routes, updatedRoute], + routes: [...state.routes, updatedRoute], index: state.routes.length, }; } From f9f7710798cc732fe7b0aeaec7096e2cbb71d201 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Tue, 24 Feb 2026 12:24:20 +0100 Subject: [PATCH 05/27] fix ios undefined state issue --- .../createRootStackNavigator/GetStateForActionHandlers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index 2e177c187566..6ab42de5f2ab 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -152,7 +152,6 @@ function handlePushFullscreenAction( state.preloadedRoutes?.some((preloadedRoute) => preloadedRoute.name === navigatorName); const adjustedState = shouldFilterPreloadedRoutes ? {...state, preloadedRoutes: state.preloadedRoutes.filter((preloadedRoute) => preloadedRoute.name !== navigatorName)} : state; - const stateWithNavigator = stackRouter.getStateForAction(adjustedState, action, configOptions); if (!stateWithNavigator) { From 593186cf99fbe236cf943d0bb42b060489124b2e Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Fri, 27 Feb 2026 10:41:04 +0100 Subject: [PATCH 06/27] reusage of navigators --- .../GetStateForActionHandlers.ts | 52 ++++++++++--- .../RootStackRouter.ts | 27 ++++++- src/libs/Navigation/Navigation.ts | 4 + src/libs/Navigation/NavigationRoot.tsx | 75 ++++++++++++++----- tests/navigation/NavigateTests.tsx | 1 + 5 files changed, 129 insertions(+), 30 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index 6ab42de5f2ab..430b9595c3f4 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -122,46 +122,74 @@ function handlePushFullscreenAction( ) { const navigatorName = action.payload.name; const params = action.payload.params; - const targetScreen = params && 'screen' in params ? (params.screen as string) : undefined; - const existingRoute = state.routes.find((route) => route.name === navigatorName); + let currentState = state; + const lastRoute = state.routes.at(-1); + + + if (lastRoute?.params && typeof (lastRoute.params as any).originalIndex === 'number') { + const originalIndex = (lastRoute.params as any).originalIndex; + const routes = [...state.routes]; + + const [movedRoute] = routes.splice(-1, 1); + + const { originalIndex: _, ...cleanParams } = (movedRoute.params as any); + const restoredRoute = { ...movedRoute, params: cleanParams }; + + routes.splice(originalIndex, 0, restoredRoute); + + currentState = { + ...state, + routes, + index: routes.length - 1, + }; + } + + const existingRoute = currentState.routes.find((route) => route.name === navigatorName); if (existingRoute) { if (existingRoute.key && targetScreen && !SCREENS_WITH_NAVIGATION_TAB_BAR.includes(targetScreen)) { screensWithEnteringAnimation.add(existingRoute.key); } + const currentPos = currentState.routes.indexOf(existingRoute); + const updatedRoute = { ...existingRoute, - params: {...existingRoute.params, ...params}, + params: { + ...existingRoute.params, + ...params, + originalIndex: currentPos + }, }; + const otherRoutes = currentState.routes.filter((route) => route.key !== existingRoute.key); + return { - ...state, - routes: [...state.routes, updatedRoute], - index: state.routes.length, + ...currentState, + routes: [...otherRoutes, updatedRoute], + index: otherRoutes.length, }; } - // If we navigate to the central screen of the split navigator, we need to filter this navigator from preloadedRoutes to remove a sidebar screen from the state const shouldFilterPreloadedRoutes = getIsNarrowLayout() && isSplitNavigatorName(navigatorName) && targetScreen !== SPLIT_TO_SIDEBAR[navigatorName] && - state.preloadedRoutes?.some((preloadedRoute) => preloadedRoute.name === navigatorName); + currentState.preloadedRoutes?.some((preloadedRoute) => preloadedRoute.name === navigatorName); + + const adjustedState = shouldFilterPreloadedRoutes + ? { ...currentState, preloadedRoutes: currentState.preloadedRoutes.filter((r) => r.name !== navigatorName) } + : currentState; - const adjustedState = shouldFilterPreloadedRoutes ? {...state, preloadedRoutes: state.preloadedRoutes.filter((preloadedRoute) => preloadedRoute.name !== navigatorName)} : state; const stateWithNavigator = stackRouter.getStateForAction(adjustedState, action, configOptions); if (!stateWithNavigator) { - Log.hmmm(`[handlePushAction] ${navigatorName} has not been found in the navigation state.`); return null; } const lastFullScreenRoute = stateWithNavigator.routes.at(-1); - - // Transitioning to all central screens in each split should be animated if (lastFullScreenRoute?.key && targetScreen && !SCREENS_WITH_NAVIGATION_TAB_BAR.includes(targetScreen)) { screensWithEnteringAnimation.add(lastFullScreenRoute.key); } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts index 8eceff88a361..4e233e6def84 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts @@ -113,10 +113,35 @@ function isNavigatingToModalFromModal(state: StackNavigationState function RootStackRouter(options: RootStackNavigatorRouterOptions) { const stackRouter = StackRouter(options); - + debugger; return { ...stackRouter, getStateForAction(state: StackNavigationState, action: RootStackNavigatorAction, configOptions: RouterConfigOptions) { + debugger; + if (action.type === 'POP' || action.type === 'GO_BACK') { + const lastRoute = state.routes.at(-1); + + if (lastRoute?.params && typeof (lastRoute.params as any).originalIndex === 'number') { + const originalIndex = (lastRoute.params as any).originalIndex; + + const newState = stackRouter.getStateForAction(state, action, configOptions); + if (!newState) return null; + + const routes = [...newState.routes]; + + const { originalIndex: _, ...cleanParams } = (lastRoute.params as any); + const restoredRoute = { ...lastRoute, params: cleanParams }; + + routes.splice(originalIndex, 0, restoredRoute); + + return { + ...newState, + routes, + index: routes.length - 1, + }; + } + } + // Evaluate navigation guards FIRST const guardState = handleNavigationGuards(state, action, configOptions, stackRouter); if (guardState) { diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 6026d5a488bf..a301ea46bb3e 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -404,6 +404,7 @@ const defaultGoBackOptions: Required = { * @param options - Optional configuration that affects navigation logic, such as parameter comparison. */ function goUp(backToRoute: Route, options?: GoBackOptions) { + debugger; if (!canNavigate('goUp', {backToRoute}) || !navigationRef.current) { Log.hmmm(`[Navigation] Unable to go up. Can't navigate.`); return; @@ -457,6 +458,7 @@ function goUp(backToRoute: Route, options?: GoBackOptions) { * @param options - Optional configuration that affects navigation logic */ function goBack(backToRoute?: Route, options?: GoBackOptions) { + debugger; clearSelectedText(); if (!canNavigate('goBack', {backToRoute})) { @@ -487,6 +489,8 @@ function goBack(backToRoute?: Route, options?: GoBackOptions) { * see the NAVIGATION.md documentation. */ function popToSidebar() { + + debugger; setShouldPopToSidebar(false); const rootState = navigationRef.current?.getRootState(); diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 2844878af164..67471b4a1c00 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -1,10 +1,10 @@ -import type {NavigationState} from '@react-navigation/native'; -import {DarkTheme, DefaultTheme, findFocusedRoute, NavigationContainer} from '@react-navigation/native'; -import {hasCompletedGuidedSetupFlowSelector} from '@selectors/Onboarding'; +import type { NavigationState } from '@react-navigation/native'; +import { DarkTheme, DefaultTheme, findFocusedRoute, NavigationContainer } from '@react-navigation/native'; +import { hasCompletedGuidedSetupFlowSelector } from '@selectors/Onboarding'; import * as Sentry from '@sentry/react-native'; -import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; -import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; -import {useCurrentReportIDActions} from '@hooks/useCurrentReportID'; +import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; +import { ScrollOffsetContext } from '@components/ScrollOffsetContextProvider'; +import { useCurrentReportIDActions } from '@hooks/useCurrentReportID'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -13,24 +13,59 @@ import useThemePreference from '@hooks/useThemePreference'; import FS from '@libs/Fullstory'; import Log from '@libs/Log'; import shouldOpenLastVisitedPath from '@libs/shouldOpenLastVisitedPath'; -import {getPathFromURL} from '@libs/Url'; -import {updateLastVisitedPath} from '@userActions/App'; -import {updateOnboardingLastVisitedPath} from '@userActions/Welcome'; +import { getPathFromURL } from '@libs/Url'; +import { updateLastVisitedPath } from '@userActions/App'; +import { updateOnboardingLastVisitedPath } from '@userActions/Welcome'; import CONST from '@src/CONST'; -import {endSpan, getSpan, startSpan} from '@src/libs/telemetry/activeSpans'; -import {navigationIntegration} from '@src/libs/telemetry/integrations'; +import { endSpan, getSpan, startSpan } from '@src/libs/telemetry/activeSpans'; +import { navigationIntegration } from '@src/libs/telemetry/integrations'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; +import type { Route } from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import AppNavigator from './AppNavigator'; -import {cleanPreservedNavigatorStates} from './AppNavigator/createSplitNavigator/usePreserveNavigatorState'; +import { cleanPreservedNavigatorStates } from './AppNavigator/createSplitNavigator/usePreserveNavigatorState'; import getAdaptedStateFromPath from './helpers/getAdaptedStateFromPath'; import getPathFromState from './helpers/getPathFromState'; -import {isSplitNavigatorName, isWorkspacesTabScreenName} from './helpers/isNavigatorName'; -import {saveSettingsTabPathToSessionStorage, saveWorkspacesTabPathToSessionStorage} from './helpers/lastVisitedTabPathUtils'; -import {linkingConfig} from './linkingConfig'; -import Navigation, {navigationRef} from './Navigation'; +import { isSplitNavigatorName, isWorkspacesTabScreenName } from './helpers/isNavigatorName'; +import { saveSettingsTabPathToSessionStorage, saveWorkspacesTabPathToSessionStorage } from './helpers/lastVisitedTabPathUtils'; +import { linkingConfig } from './linkingConfig'; +import Navigation, { navigationRef } from './Navigation'; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + type NavigationRootProps = { /** Whether the current user is logged in with an authToken */ @@ -234,9 +269,15 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N const currentRoute = navigationRef.getCurrentRoute(); Sentry.addBreadcrumb({message: `[NAVIGATION] screen: ${currentRoute?.name}, params: ${JSON.stringify(currentRoute?.params ?? {})}`, category: 'navigation'}); + console.log('__________________'); + console.log(state); + console.log("__________________") + + updateCurrentReportID(state); parseAndLogRoute(state); + // We want to clean saved scroll offsets for screens that aren't anymore in the state. cleanStaleScrollOffsets(state); cleanPreservedNavigatorStates(state); diff --git a/tests/navigation/NavigateTests.tsx b/tests/navigation/NavigateTests.tsx index 636b5995178e..008cec2fd979 100644 --- a/tests/navigation/NavigateTests.tsx +++ b/tests/navigation/NavigateTests.tsx @@ -210,6 +210,7 @@ describe('Navigate', () => { name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, state: { index: 0, + stale: false, routes: [ { name: SCREENS.SETTINGS.ROOT, From bf33da107ac514b14e4feb9af7f49e2d30581446 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Fri, 27 Feb 2026 10:52:29 +0100 Subject: [PATCH 07/27] migrate state editing only to place where it is rendering --- .../createRootStackNavigator/index.tsx | 6 ++-- .../useCustomRootStackNavigatorState/index.ts | 31 ++++++++++++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx index b6138ecd540c..af6a121cd86f 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx @@ -1,10 +1,10 @@ -import type {NavigationProp, NavigatorTypeBagBase, ParamListBase, StaticConfig, TypedNavigator} from '@react-navigation/native'; -import {createNavigatorFactory} from '@react-navigation/native'; +import type { NavigationProp, NavigatorTypeBagBase, ParamListBase, StaticConfig, TypedNavigator } from '@react-navigation/native'; +import { createNavigatorFactory } from '@react-navigation/native'; import RootNavigatorExtraContent from '@components/Navigation/RootNavigatorExtraContent'; import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange'; import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent'; import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions'; -import type {PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types'; +import type { PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState } from '@libs/Navigation/PlatformStackNavigation/types'; import RootStackRouter from './RootStackRouter'; import useCustomRootStackNavigatorState from './useCustomRootStackNavigatorState'; diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts index 88a7135aa0b3..8941a5a4e832 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts @@ -1,18 +1,39 @@ import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; -// This is an optimization to keep mounted only last few screens in the stack. export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); let indexToSlice = Math.max(0, lastSplitIndex); const hasPrevRoute = lastSplitIndex > 0; const isPrevFullScreen = isFullScreenName(state.routes.at(lastSplitIndex - 1)?.name); - // If the route before the last full screen is e.g. RHP, we should leave it in the rendered routes, - // as there may be display issues (blank screen) when navigating back and recreating that route to render. if (hasPrevRoute && !isPrevFullScreen) { indexToSlice = lastSplitIndex - 1; } - const routesToRender = state.routes.slice(indexToSlice, state.routes.length); - return {...state, routes: routesToRender, index: routesToRender.length - 1}; + + + const firstKeyMap = new Map(); + state.routes.forEach((route) => { + if (!firstKeyMap.has(route.name)) { + firstKeyMap.set(route.name, route.key); + } + }); + + const routesToRender = state.routes.slice(indexToSlice).map((route) => { + const firstKey = firstKeyMap.get(route.name); + + if (firstKey && route.key !== firstKey) { + return { + ...route, + key: firstKey, + }; + } + return route; + }); + + return { + ...state, + routes: routesToRender, + index: routesToRender.length - 1 + }; } From 9d76506c07d62ab0923208bf939cb412c2e82923 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Fri, 27 Feb 2026 10:56:45 +0100 Subject: [PATCH 08/27] remove unnecessary changes with broke history --- .../GetStateForActionHandlers.ts | 112 ++++++++---------- .../RootStackRouter.ts | 25 ---- src/libs/Navigation/Navigation.ts | 3 - 3 files changed, 51 insertions(+), 89 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index 430b9595c3f4..19cc5e95604b 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -1,15 +1,57 @@ -import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; -import {StackActions} from '@react-navigation/native'; -import type {ParamListBase, Router} from '@react-navigation/routers'; +import type { CommonActions, RouterConfigOptions, StackActionType, StackNavigationState } from '@react-navigation/native'; +import { StackActions } from '@react-navigation/native'; +import type { ParamListBase, Router } from '@react-navigation/routers'; import SCREENS_WITH_NAVIGATION_TAB_BAR from '@components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Log from '@libs/Log'; -import {isSplitNavigatorName} from '@libs/Navigation/helpers/isNavigatorName'; -import {SPLIT_TO_SIDEBAR} from '@libs/Navigation/linkingConfig/RELATIONS'; +import { isSplitNavigatorName } from '@libs/Navigation/helpers/isNavigatorName'; +import { SPLIT_TO_SIDEBAR } from '@libs/Navigation/linkingConfig/RELATIONS'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; -import type {OpenDomainSplitActionType, OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType} from './types'; +import type { OpenDomainSplitActionType, OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType } from './types'; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + const MODAL_ROUTES_TO_DISMISS = new Set([ NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, @@ -121,71 +163,19 @@ function handlePushFullscreenAction( stackRouter: Router, CommonActions.Action | StackActionType>, ) { const navigatorName = action.payload.name; - const params = action.payload.params; - const targetScreen = params && 'screen' in params ? (params.screen as string) : undefined; - - let currentState = state; - const lastRoute = state.routes.at(-1); - - - if (lastRoute?.params && typeof (lastRoute.params as any).originalIndex === 'number') { - const originalIndex = (lastRoute.params as any).originalIndex; - const routes = [...state.routes]; - - const [movedRoute] = routes.splice(-1, 1); - - const { originalIndex: _, ...cleanParams } = (movedRoute.params as any); - const restoredRoute = { ...movedRoute, params: cleanParams }; - - routes.splice(originalIndex, 0, restoredRoute); - - currentState = { - ...state, - routes, - index: routes.length - 1, - }; - } - - const existingRoute = currentState.routes.find((route) => route.name === navigatorName); - - if (existingRoute) { - if (existingRoute.key && targetScreen && !SCREENS_WITH_NAVIGATION_TAB_BAR.includes(targetScreen)) { - screensWithEnteringAnimation.add(existingRoute.key); - } - - const currentPos = currentState.routes.indexOf(existingRoute); - - const updatedRoute = { - ...existingRoute, - params: { - ...existingRoute.params, - ...params, - originalIndex: currentPos - }, - }; - - const otherRoutes = currentState.routes.filter((route) => route.key !== existingRoute.key); - - return { - ...currentState, - routes: [...otherRoutes, updatedRoute], - index: otherRoutes.length, - }; - } const shouldFilterPreloadedRoutes = getIsNarrowLayout() && isSplitNavigatorName(navigatorName) && targetScreen !== SPLIT_TO_SIDEBAR[navigatorName] && - currentState.preloadedRoutes?.some((preloadedRoute) => preloadedRoute.name === navigatorName); + state.preloadedRoutes?.some((preloadedRoute) => preloadedRoute.name === navigatorName); - const adjustedState = shouldFilterPreloadedRoutes - ? { ...currentState, preloadedRoutes: currentState.preloadedRoutes.filter((r) => r.name !== navigatorName) } - : currentState; + const adjustedState = shouldFilterPreloadedRoutes ? {...state, preloadedRoutes: state.preloadedRoutes.filter((preloadedRoute) => preloadedRoute.name !== navigatorName)} : state; const stateWithNavigator = stackRouter.getStateForAction(adjustedState, action, configOptions); if (!stateWithNavigator) { + Log.hmmm(`[handlePushAction] ${navigatorName} has not been found in the navigation state.`); return null; } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts index 4e233e6def84..c1fe00c1dfa0 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts @@ -113,34 +113,9 @@ function isNavigatingToModalFromModal(state: StackNavigationState function RootStackRouter(options: RootStackNavigatorRouterOptions) { const stackRouter = StackRouter(options); - debugger; return { ...stackRouter, getStateForAction(state: StackNavigationState, action: RootStackNavigatorAction, configOptions: RouterConfigOptions) { - debugger; - if (action.type === 'POP' || action.type === 'GO_BACK') { - const lastRoute = state.routes.at(-1); - - if (lastRoute?.params && typeof (lastRoute.params as any).originalIndex === 'number') { - const originalIndex = (lastRoute.params as any).originalIndex; - - const newState = stackRouter.getStateForAction(state, action, configOptions); - if (!newState) return null; - - const routes = [...newState.routes]; - - const { originalIndex: _, ...cleanParams } = (lastRoute.params as any); - const restoredRoute = { ...lastRoute, params: cleanParams }; - - routes.splice(originalIndex, 0, restoredRoute); - - return { - ...newState, - routes, - index: routes.length - 1, - }; - } - } // Evaluate navigation guards FIRST const guardState = handleNavigationGuards(state, action, configOptions, stackRouter); diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index a301ea46bb3e..8009846422ef 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -404,7 +404,6 @@ const defaultGoBackOptions: Required = { * @param options - Optional configuration that affects navigation logic, such as parameter comparison. */ function goUp(backToRoute: Route, options?: GoBackOptions) { - debugger; if (!canNavigate('goUp', {backToRoute}) || !navigationRef.current) { Log.hmmm(`[Navigation] Unable to go up. Can't navigate.`); return; @@ -458,7 +457,6 @@ function goUp(backToRoute: Route, options?: GoBackOptions) { * @param options - Optional configuration that affects navigation logic */ function goBack(backToRoute?: Route, options?: GoBackOptions) { - debugger; clearSelectedText(); if (!canNavigate('goBack', {backToRoute})) { @@ -490,7 +488,6 @@ function goBack(backToRoute?: Route, options?: GoBackOptions) { */ function popToSidebar() { - debugger; setShouldPopToSidebar(false); const rootState = navigationRef.current?.getRootState(); From 3a4a22ecdb8e1375864bb3cc09c4988ff17d03ec Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Fri, 27 Feb 2026 12:36:34 +0100 Subject: [PATCH 09/27] empty screens after returning to scream --- .../GetStateForActionHandlers.ts | 55 ++------------ .../useCustomRootStackNavigatorState/index.ts | 45 +++++++---- src/libs/Navigation/NavigationRoot.tsx | 75 +++++-------------- 3 files changed, 55 insertions(+), 120 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index 19cc5e95604b..52a53673ec17 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -1,57 +1,15 @@ -import type { CommonActions, RouterConfigOptions, StackActionType, StackNavigationState } from '@react-navigation/native'; -import { StackActions } from '@react-navigation/native'; -import type { ParamListBase, Router } from '@react-navigation/routers'; +import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; +import {StackActions} from '@react-navigation/native'; +import type {ParamListBase, Router} from '@react-navigation/routers'; import SCREENS_WITH_NAVIGATION_TAB_BAR from '@components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Log from '@libs/Log'; -import { isSplitNavigatorName } from '@libs/Navigation/helpers/isNavigatorName'; -import { SPLIT_TO_SIDEBAR } from '@libs/Navigation/linkingConfig/RELATIONS'; +import {isSplitNavigatorName} from '@libs/Navigation/helpers/isNavigatorName'; +import {SPLIT_TO_SIDEBAR} from '@libs/Navigation/linkingConfig/RELATIONS'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; -import type { OpenDomainSplitActionType, OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType } from './types'; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +import type {OpenDomainSplitActionType, OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType} from './types'; const MODAL_ROUTES_TO_DISMISS = new Set([ NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, @@ -163,6 +121,7 @@ function handlePushFullscreenAction( stackRouter: Router, CommonActions.Action | StackActionType>, ) { const navigatorName = action.payload.name; + const targetScreen = action.payload?.params && 'screen' in action.payload.params ? (action.payload?.params?.screen as string) : undefined; const shouldFilterPreloadedRoutes = getIsNarrowLayout() && diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts index 8941a5a4e832..62bd421bc3b3 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts @@ -1,39 +1,56 @@ +import {screensWithEnteringAnimation, workspaceOrDomainSplitsWithoutEnteringAnimation} from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; +// This is an optimization to keep mounted only last few screens in the stack. export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); let indexToSlice = Math.max(0, lastSplitIndex); const hasPrevRoute = lastSplitIndex > 0; const isPrevFullScreen = isFullScreenName(state.routes.at(lastSplitIndex - 1)?.name); + // If the route before the last full screen is e.g. RHP, we should leave it in the rendered routes, + // as there may be display issues (blank screen) when navigating back and recreating that route to render. if (hasPrevRoute && !isPrevFullScreen) { indexToSlice = lastSplitIndex - 1; } - + // Build map: navigator name → key of first occurrence in the full (unsliced) state. + // We need the full state so we find the first key even if it was sliced out. const firstKeyMap = new Map(); - state.routes.forEach((route) => { - if (!firstKeyMap.has(route.name)) { - firstKeyMap.set(route.name, route.key); + for (const route of state.routes) { + if (firstKeyMap.has(route.name)) { + continue; } - }); + firstKeyMap.set(route.name, route.key); + } const routesToRender = state.routes.slice(indexToSlice).map((route) => { + // Only remap keys for fullscreen (split) navigators. Intermediate screens like WorkspacesList + // must keep their own keys — remapping them causes the rendered state to look identical to + // the previous render, so React Navigation skips the transition and shows a blank screen. + if (!isFullScreenName(route.name)) { + return route; + } + const firstKey = firstKeyMap.get(route.name); if (firstKey && route.key !== firstKey) { - return { - ...route, - key: firstKey, - }; + // Sync animation sets: the sets track keys added during getStateForAction (router level). + // Since rendered key changes from route.key → firstKey, we must remap the set entries + // so animation config (getFullscreenNavigatorOptions) picks up the right key. + if (screensWithEnteringAnimation.has(route.key)) { + screensWithEnteringAnimation.delete(route.key); + screensWithEnteringAnimation.add(firstKey); + } + if (workspaceOrDomainSplitsWithoutEnteringAnimation.has(route.key)) { + workspaceOrDomainSplitsWithoutEnteringAnimation.delete(route.key); + workspaceOrDomainSplitsWithoutEnteringAnimation.add(firstKey); + } + return {...route, key: firstKey}; } return route; }); - return { - ...state, - routes: routesToRender, - index: routesToRender.length - 1 - }; + return {...state, routes: routesToRender, index: routesToRender.length - 1}; } diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 67471b4a1c00..2844878af164 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -1,10 +1,10 @@ -import type { NavigationState } from '@react-navigation/native'; -import { DarkTheme, DefaultTheme, findFocusedRoute, NavigationContainer } from '@react-navigation/native'; -import { hasCompletedGuidedSetupFlowSelector } from '@selectors/Onboarding'; +import type {NavigationState} from '@react-navigation/native'; +import {DarkTheme, DefaultTheme, findFocusedRoute, NavigationContainer} from '@react-navigation/native'; +import {hasCompletedGuidedSetupFlowSelector} from '@selectors/Onboarding'; import * as Sentry from '@sentry/react-native'; -import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; -import { ScrollOffsetContext } from '@components/ScrollOffsetContextProvider'; -import { useCurrentReportIDActions } from '@hooks/useCurrentReportID'; +import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; +import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; +import {useCurrentReportIDActions} from '@hooks/useCurrentReportID'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -13,59 +13,24 @@ import useThemePreference from '@hooks/useThemePreference'; import FS from '@libs/Fullstory'; import Log from '@libs/Log'; import shouldOpenLastVisitedPath from '@libs/shouldOpenLastVisitedPath'; -import { getPathFromURL } from '@libs/Url'; -import { updateLastVisitedPath } from '@userActions/App'; -import { updateOnboardingLastVisitedPath } from '@userActions/Welcome'; +import {getPathFromURL} from '@libs/Url'; +import {updateLastVisitedPath} from '@userActions/App'; +import {updateOnboardingLastVisitedPath} from '@userActions/Welcome'; import CONST from '@src/CONST'; -import { endSpan, getSpan, startSpan } from '@src/libs/telemetry/activeSpans'; -import { navigationIntegration } from '@src/libs/telemetry/integrations'; +import {endSpan, getSpan, startSpan} from '@src/libs/telemetry/activeSpans'; +import {navigationIntegration} from '@src/libs/telemetry/integrations'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type { Route } from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import AppNavigator from './AppNavigator'; -import { cleanPreservedNavigatorStates } from './AppNavigator/createSplitNavigator/usePreserveNavigatorState'; +import {cleanPreservedNavigatorStates} from './AppNavigator/createSplitNavigator/usePreserveNavigatorState'; import getAdaptedStateFromPath from './helpers/getAdaptedStateFromPath'; import getPathFromState from './helpers/getPathFromState'; -import { isSplitNavigatorName, isWorkspacesTabScreenName } from './helpers/isNavigatorName'; -import { saveSettingsTabPathToSessionStorage, saveWorkspacesTabPathToSessionStorage } from './helpers/lastVisitedTabPathUtils'; -import { linkingConfig } from './linkingConfig'; -import Navigation, { navigationRef } from './Navigation'; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +import {isSplitNavigatorName, isWorkspacesTabScreenName} from './helpers/isNavigatorName'; +import {saveSettingsTabPathToSessionStorage, saveWorkspacesTabPathToSessionStorage} from './helpers/lastVisitedTabPathUtils'; +import {linkingConfig} from './linkingConfig'; +import Navigation, {navigationRef} from './Navigation'; type NavigationRootProps = { /** Whether the current user is logged in with an authToken */ @@ -269,15 +234,9 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N const currentRoute = navigationRef.getCurrentRoute(); Sentry.addBreadcrumb({message: `[NAVIGATION] screen: ${currentRoute?.name}, params: ${JSON.stringify(currentRoute?.params ?? {})}`, category: 'navigation'}); - console.log('__________________'); - console.log(state); - console.log("__________________") - - updateCurrentReportID(state); parseAndLogRoute(state); - // We want to clean saved scroll offsets for screens that aren't anymore in the state. cleanStaleScrollOffsets(state); cleanPreservedNavigatorStates(state); From fdcb6af03bfd7bd680e638d3d8943b4b56a0b953 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Fri, 27 Feb 2026 15:12:21 +0100 Subject: [PATCH 10/27] work in progress --- .../reuseNavigatorKey.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts new file mode 100644 index 000000000000..30f87a5d7839 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -0,0 +1,118 @@ +import type { ParamListBase, StackNavigationState } from '@react-navigation/native'; +import { screensWithEnteringAnimation } from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; +import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; + + + + + + +type StateRoutes = StackNavigationState['routes']; + +/** + * Remaps the key of a fullscreen navigator route in `routesToRender` to the canonical (first) occurrence key + * of that navigator name found in the full navigation state. + * + * This allows React to reuse the already-mounted navigator component instead of remounting it, + * avoiding expensive teardown/setup cycles. The real navigation state is left untouched, so + * browser history still works correctly (the state always has unique keys). + * + * Example: state=[RSN-key1, WSN-key2, RSN-key3], routesToRender=[WSN-key2, RSN-key3] + * → RSN-kopey3 is remapped to RSN-key1 (the first RSN in state, which is already mounted) + * → React sees [WSN-key2, RSN-key1] in both old and new render → reuses RSN-key1 component + */ +function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigationState): StateRoutes { + debugger; + return routesToRender.map((route) => { + if (!isFullScreenName(route.name)) { + return route; + } + + const firstOccurrence = fullState.routes.find((r) => r.name === route.name); + if (!firstOccurrence) { + return route; + } + + const canonicalKey = firstOccurrence.key; + const shouldRemap = canonicalKey !== route.key && !routesToRender.some((r) => r.key === canonicalKey); + + if (!shouldRemap) { + return route; + } + + // Transfer animation marker from new key to canonical key so slide-in animation fires correctly. + if (screensWithEnteringAnimation.has(route.key)) { + screensWithEnteringAnimation.delete(route.key); + screensWithEnteringAnimation.add(canonicalKey); + } + + return {...firstOccurrence, params: {...firstOccurrence.params, ...route.params}}; + }); +} + +function cleanNavigationState(state) { + function processNavigator(navigatorState) { + if (!navigatorState?.routes) { + return navigatorState; + } + + const seenNames = new Map(); // name -> canonicalKey + const keyRemap = new Map(); // oldKey -> canonicalKey + const cleanedRoutesReversed = []; + + // 1️⃣ iterujemy OD KOŃCA (nowsze mają priorytet) + for (let i = navigatorState.routes.length - 1; i >= 0; i--) { + const route = navigatorState.routes[i]; + + let nextRoute = route; + + // rekurencja w dół + if (route.state) { + nextRoute = { + ...route, + state: processNavigator(route.state), + }; + } + + const canonicalKey = seenNames.get(route.name); + + if (canonicalKey) { + // duplikat → zachowujemy nowszy route, + // ale mapujemy key na wcześniejszy + keyRemap.set(route.key, canonicalKey); + + cleanedRoutesReversed.push({ + ...nextRoute, + key: canonicalKey, + }); + } else { + // pierwsze wystąpienie (od końca) + seenNames.set(route.name, route.key); + cleanedRoutesReversed.push(nextRoute); + } + } + + const cleanedRoutes = cleanedRoutesReversed.reverse(); + const validKeys = new Set(cleanedRoutes.map((r) => r.key)); + + // 2️⃣ naprawa history + let cleanedHistory = navigatorState.history; + if (Array.isArray(navigatorState.history)) { + cleanedHistory = navigatorState.history.map((key) => keyRemap.get(key) ?? key).filter((key) => validKeys.has(key)); + } + + // 3️⃣ naprawa index + const cleanedIndex = Math.min(navigatorState.index ?? 0, Math.max(cleanedRoutes.length - 1, 0)); + + return { + ...navigatorState, + routes: cleanedRoutes, + history: cleanedHistory, + index: cleanedIndex, + }; + } + + return processNavigator(state); +} + +export {reuseNavigatorKey,cleanNavigationState}; From d8ebd3f807d2178c1558eaa3a34b9f47e7c487e8 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 2 Mar 2026 08:18:23 +0100 Subject: [PATCH 11/27] working history but with week performance --- .../index.android.ts | 14 +++----- .../index.ios.ts | 4 ++- .../useCustomRootStackNavigatorState/index.ts | 35 +++++++++++++++---- .../reuseNavigatorKey.ts | 5 +-- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.android.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.android.ts index 88a7135aa0b3..c060873e981f 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.android.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.android.ts @@ -1,18 +1,14 @@ import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import reuseNavigatorKey from './reuseNavigatorKey'; // This is an optimization to keep mounted only last few screens in the stack. export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); - let indexToSlice = Math.max(0, lastSplitIndex); - const hasPrevRoute = lastSplitIndex > 0; - const isPrevFullScreen = isFullScreenName(state.routes.at(lastSplitIndex - 1)?.name); - // If the route before the last full screen is e.g. RHP, we should leave it in the rendered routes, - // as there may be display issues (blank screen) when navigating back and recreating that route to render. - if (hasPrevRoute && !isPrevFullScreen) { - indexToSlice = lastSplitIndex - 1; - } + // Always preserve the previous fullscreen route so React can reuse the mounted navigator component on navigation. + const indexToSlice = Math.max(0, lastSplitIndex - 1); const routesToRender = state.routes.slice(indexToSlice, state.routes.length); - return {...state, routes: routesToRender, index: routesToRender.length - 1}; + const remappedRoutes = reuseNavigatorKey(routesToRender, state); + return {...state, routes: remappedRoutes, index: remappedRoutes.length - 1}; } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ios.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ios.ts index 0351251d6871..b23fffcb7893 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ios.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ios.ts @@ -4,6 +4,7 @@ import {SPLIT_TO_SIDEBAR} from '@libs/Navigation/linkingConfig/RELATIONS'; import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {NavigationRoute, SplitNavigatorName} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; +import reuseNavigatorKey from './reuseNavigatorKey'; // Swiping back on iOS does not work properly when the preloaded route has gestureEnabled set to false. // Therefore, on screens where swiping should work, preloadedRoutes will be an empty array during rendering to ensure swiping works properly. @@ -36,7 +37,8 @@ function getShouldHidePreloadedRoutes(route?: NavigationRoute) { export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); const routesToRender = state.routes.slice(Math.max(0, lastSplitIndex - 1), state.routes.length); - const stateToRender = {...state, routes: routesToRender, index: routesToRender.length - 1}; + const remappedRoutes = reuseNavigatorKey(routesToRender, state); + const stateToRender = {...state, routes: remappedRoutes, index: remappedRoutes.length - 1}; if (getShouldHidePreloadedRoutes(stateToRender.routes.at(-1))) { return {...stateToRender, preloadedRoutes: []}; } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts index 88a7135aa0b3..0aa4d900220e 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts @@ -1,18 +1,41 @@ -import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; -import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; +import type { CustomStateHookProps } from '@libs/Navigation/PlatformStackNavigation/types'; +import reuseNavigatorKey, { cleanNavigationState } from './reuseNavigatorKey'; + + + + + + + + + + + + + + + + + + + + // This is an optimization to keep mounted only last few screens in the stack. export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { - const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); + + const cleanState = cleanNavigationState(state) + const lastSplitIndex = cleanState.routes.findLastIndex((route) => isFullScreenName(route.name)); let indexToSlice = Math.max(0, lastSplitIndex); const hasPrevRoute = lastSplitIndex > 0; - const isPrevFullScreen = isFullScreenName(state.routes.at(lastSplitIndex - 1)?.name); + const isPrevFullScreen = isFullScreenName(cleanState.routes.at(lastSplitIndex - 1)?.name); // If the route before the last full screen is e.g. RHP, we should leave it in the rendered routes, // as there may be display issues (blank screen) when navigating back and recreating that route to render. if (hasPrevRoute && !isPrevFullScreen) { indexToSlice = lastSplitIndex - 1; } - const routesToRender = state.routes.slice(indexToSlice, state.routes.length); - return {...state, routes: routesToRender, index: routesToRender.length - 1}; + const routesToRender = cleanState.routes.slice(indexToSlice, cleanState.routes.length); + return {...cleanState, routes: routesToRender, index: routesToRender.length - 1}; } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index 30f87a5d7839..b50ff11d42de 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -111,8 +111,9 @@ function cleanNavigationState(state) { index: cleanedIndex, }; } - - return processNavigator(state); + const result = processNavigator(state); + debugger; + return result } export {reuseNavigatorKey,cleanNavigationState}; From 70e834a0d597c2c0fddb2fcbe7f1a6eb91e50793 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 2 Mar 2026 11:30:58 +0100 Subject: [PATCH 12/27] performance is great again, last bug when returning to split navigators switching tabs does not work --- .../FreezeWrapper/getIsScreenBlurred/index.ts | 21 ++- .../useCustomRootStackNavigatorState/index.ts | 41 ++---- .../reuseNavigatorKey.ts | 120 ++++-------------- .../createSplitNavigator/index.tsx | 2 + 4 files changed, 61 insertions(+), 123 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.ts b/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.ts index 0b51e817a0ba..12e12543c4cc 100644 --- a/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.ts +++ b/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.ts @@ -3,7 +3,26 @@ import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; function getIsScreenBlurred(state: NavigationState, currentRouteKey: string) { const lastFullScreenRoute = state.routes.findLast((route) => isFullScreenName(route.name)); - return lastFullScreenRoute?.key !== currentRouteKey; + + if (!lastFullScreenRoute) { + return false; + } + + // Direct key match — screen is focused. + if (lastFullScreenRoute.key === currentRouteKey) { + return false; + } + + // Key mismatch fallback: check if the current route and the focused route are the same navigator type. + // This handles the key-reuse optimization in useCustomRootStackNavigatorState where the rendered + // route keeps an old canonical key while the real navigation state has a newer key for the same + // navigator type. Without this check, FreezeWrapper would incorrectly freeze the active screen. + const currentRouteInState = state.routes.find((r) => r.key === currentRouteKey); + if (currentRouteInState?.name === lastFullScreenRoute.name) { + return false; + } + + return true; } export default getIsScreenBlurred; diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts index 0aa4d900220e..320de6c9a9f9 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts @@ -1,41 +1,24 @@ -import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; -import type { CustomStateHookProps } from '@libs/Navigation/PlatformStackNavigation/types'; -import reuseNavigatorKey, { cleanNavigationState } from './reuseNavigatorKey'; - - - - - - - - - - - - - - - - - - - - +import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; +import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import reuseNavigatorKey from './reuseNavigatorKey'; // This is an optimization to keep mounted only last few screens in the stack. export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { - - const cleanState = cleanNavigationState(state) - const lastSplitIndex = cleanState.routes.findLastIndex((route) => isFullScreenName(route.name)); + const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); let indexToSlice = Math.max(0, lastSplitIndex); const hasPrevRoute = lastSplitIndex > 0; - const isPrevFullScreen = isFullScreenName(cleanState.routes.at(lastSplitIndex - 1)?.name); + const isPrevFullScreen = isFullScreenName(state.routes.at(lastSplitIndex - 1)?.name); // If the route before the last full screen is e.g. RHP, we should leave it in the rendered routes, // as there may be display issues (blank screen) when navigating back and recreating that route to render. if (hasPrevRoute && !isPrevFullScreen) { indexToSlice = lastSplitIndex - 1; } - const routesToRender = cleanState.routes.slice(indexToSlice, cleanState.routes.length); - return {...cleanState, routes: routesToRender, index: routesToRender.length - 1}; + + const routesToRender = state.routes.slice(indexToSlice, state.routes.length); + const remappedRoutes = reuseNavigatorKey(routesToRender, state); + + debugger; + + return {...state, routes: remappedRoutes, index: remappedRoutes.length - 1}; } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index b50ff11d42de..d23c4d62e7c3 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -1,119 +1,53 @@ -import type { ParamListBase, StackNavigationState } from '@react-navigation/native'; -import { screensWithEnteringAnimation } from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; -import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; - - - - - +import type {ParamListBase, StackNavigationState} from '@react-navigation/native'; +import {screensWithEnteringAnimation} from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; +import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; type StateRoutes = StackNavigationState['routes']; /** - * Remaps the key of a fullscreen navigator route in `routesToRender` to the canonical (first) occurrence key - * of that navigator name found in the full navigation state. - * - * This allows React to reuse the already-mounted navigator component instead of remounting it, - * avoiding expensive teardown/setup cycles. The real navigation state is left untouched, so - * browser history still works correctly (the state always has unique keys). + * For each fullscreen navigator in routesToRender, checks if a route with the same name + * already exists in the full state (with a different key). If so, replaces the new route + * with the existing one updated with the new params — exactly mirroring what + * handlePushFullscreenAction did at the router level. * - * Example: state=[RSN-key1, WSN-key2, RSN-key3], routesToRender=[WSN-key2, RSN-key3] - * → RSN-kopey3 is remapped to RSN-key1 (the first RSN in state, which is already mounted) - * → React sees [WSN-key2, RSN-key1] in both old and new render → reuses RSN-key1 component + * This causes React to reuse the already-mounted navigator component (same key), + * while the new params tell the navigator which screen to show. + * The real navigation state is untouched. */ function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigationState): StateRoutes { - debugger; return routesToRender.map((route) => { if (!isFullScreenName(route.name)) { return route; } - const firstOccurrence = fullState.routes.find((r) => r.name === route.name); - if (!firstOccurrence) { + // Find a route already in the stack with the same navigator name. + // A different key means it was pushed in a previous navigation action. + const existingRoute = fullState.routes.find((r) => r.name === route.name && r.key !== route.key); + + if (!existingRoute) { return route; } - const canonicalKey = firstOccurrence.key; - const shouldRemap = canonicalKey !== route.key && !routesToRender.some((r) => r.key === canonicalKey); - - if (!shouldRemap) { + // Avoid duplicate keys in the rendered list. + if (routesToRender.some((r) => r.key === existingRoute.key)) { return route; } - // Transfer animation marker from new key to canonical key so slide-in animation fires correctly. + // Transfer animation marker so the entering animation fires on the reused component. if (screensWithEnteringAnimation.has(route.key)) { screensWithEnteringAnimation.delete(route.key); - screensWithEnteringAnimation.add(canonicalKey); - } - - return {...firstOccurrence, params: {...firstOccurrence.params, ...route.params}}; - }); -} - -function cleanNavigationState(state) { - function processNavigator(navigatorState) { - if (!navigatorState?.routes) { - return navigatorState; + screensWithEnteringAnimation.add(existingRoute.key); } - const seenNames = new Map(); // name -> canonicalKey - const keyRemap = new Map(); // oldKey -> canonicalKey - const cleanedRoutesReversed = []; - - // 1️⃣ iterujemy OD KOŃCA (nowsze mają priorytet) - for (let i = navigatorState.routes.length - 1; i >= 0; i--) { - const route = navigatorState.routes[i]; - - let nextRoute = route; - - // rekurencja w dół - if (route.state) { - nextRoute = { - ...route, - state: processNavigator(route.state), - }; - } - - const canonicalKey = seenNames.get(route.name); - - if (canonicalKey) { - // duplikat → zachowujemy nowszy route, - // ale mapujemy key na wcześniejszy - keyRemap.set(route.key, canonicalKey); - - cleanedRoutesReversed.push({ - ...nextRoute, - key: canonicalKey, - }); - } else { - // pierwsze wystąpienie (od końca) - seenNames.set(route.name, route.key); - cleanedRoutesReversed.push(nextRoute); - } - } - - const cleanedRoutes = cleanedRoutesReversed.reverse(); - const validKeys = new Set(cleanedRoutes.map((r) => r.key)); - - // 2️⃣ naprawa history - let cleanedHistory = navigatorState.history; - if (Array.isArray(navigatorState.history)) { - cleanedHistory = navigatorState.history.map((key) => keyRemap.get(key) ?? key).filter((key) => validKeys.has(key)); - } - - // 3️⃣ naprawa index - const cleanedIndex = Math.min(navigatorState.index ?? 0, Math.max(cleanedRoutes.length - 1, 0)); - + // Mirror handlePushFullscreenAction: + // spread the existing route (preserves key and navigator state) and merge new params on top. + // The existing key causes React to reuse the mounted component. + // The merged params carry the new navigation target (screen/params) into the reused component. return { - ...navigatorState, - routes: cleanedRoutes, - history: cleanedHistory, - index: cleanedIndex, + ...existingRoute, + params: {...existingRoute.params, ...route.params}, }; - } - const result = processNavigator(state); - debugger; - return result + }); } -export {reuseNavigatorKey,cleanNavigationState}; +export default reuseNavigatorKey; diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx index 4483297f6a1b..03b6d0f4a6bd 100644 --- a/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx @@ -13,11 +13,13 @@ import type { } from '@libs/Navigation/PlatformStackNavigation/types'; import SidebarSpacerWrapper from './SidebarSpacerWrapper'; import SplitRouter from './SplitRouter'; +import useNavigateOnParamsChange from './useNavigateOnParamsChange'; import usePreserveNavigatorState from './usePreserveNavigatorState'; function useCustomEffects(props: CustomEffectsHookProps) { useNavigationResetOnLayoutChange(props); usePreserveNavigatorState(props.state, props.parentRoute); + useNavigateOnParamsChange(props); } function useCustomSplitNavigatorState({state}: CustomStateHookProps) { From 09ee767a3f2bacce9a89ded9f3884ba98a342761 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 2 Mar 2026 12:40:59 +0100 Subject: [PATCH 13/27] performance is great again, fix bug when navigating on the same split navigator --- .../reuseNavigatorKey.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index d23c4d62e7c3..6633e2478401 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -1,6 +1,6 @@ -import type {ParamListBase, StackNavigationState} from '@react-navigation/native'; -import {screensWithEnteringAnimation} from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; -import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; +import type { ParamListBase, StackNavigationState } from '@react-navigation/native'; +import { screensWithEnteringAnimation } from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; +import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; type StateRoutes = StackNavigationState['routes']; @@ -9,19 +9,20 @@ type StateRoutes = StackNavigationState['routes']; * already exists in the full state (with a different key). If so, replaces the new route * with the existing one updated with the new params — exactly mirroring what * handlePushFullscreenAction did at the router level. - * * This causes React to reuse the already-mounted navigator component (same key), * while the new params tell the navigator which screen to show. * The real navigation state is untouched. */ + + + function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigationState): StateRoutes { return routesToRender.map((route) => { - if (!isFullScreenName(route.name)) { + if (!isFullScreenName(route.name) || fullState.routes.at(fullState.routes.length - 1)?.name === route.name) { return route; } - // Find a route already in the stack with the same navigator name. - // A different key means it was pushed in a previous navigation action. + // Find an already mounted route with the same navigator name const existingRoute = fullState.routes.find((r) => r.name === route.name && r.key !== route.key); if (!existingRoute) { @@ -39,13 +40,11 @@ function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigati screensWithEnteringAnimation.add(existingRoute.key); } - // Mirror handlePushFullscreenAction: - // spread the existing route (preserves key and navigator state) and merge new params on top. - // The existing key causes React to reuse the mounted component. - // The merged params carry the new navigation target (screen/params) into the reused component. return { - ...existingRoute, - params: {...existingRoute.params, ...route.params}, + ...existingRoute, // Preserve key and internal navigator state + params: { + ...route.params, // Apply params from the new navigation action + }, }; }); } From a7b74237ae40c4afa85b47c0107a53b7f8842e37 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 2 Mar 2026 13:08:56 +0100 Subject: [PATCH 14/27] refactor reuseNavigatorKey function --- .../reuseNavigatorKey.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index 6633e2478401..dac2390e5b9a 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -5,45 +5,49 @@ import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; type StateRoutes = StackNavigationState['routes']; /** - * For each fullscreen navigator in routesToRender, checks if a route with the same name - * already exists in the full state (with a different key). If so, replaces the new route - * with the existing one updated with the new params — exactly mirroring what - * handlePushFullscreenAction did at the router level. - * This causes React to reuse the already-mounted navigator component (same key), - * while the new params tell the navigator which screen to show. - * The real navigation state is untouched. + * For each fullscreen navigator in routesToRender, checks whether a navigator + * with the same name is already mounted in the current navigation state. + * + * If such a route exists (with a different key), the new route is replaced + * with the existing one while applying the new params. + * + * This preserves the original route key so React can reuse the already-mounted + * navigator instance, while the updated params instruct it which screen to display. + * + * The actual navigation state is not mutated — this only affects what gets rendered. */ - - - function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigationState): StateRoutes { return routesToRender.map((route) => { - if (!isFullScreenName(route.name) || fullState.routes.at(fullState.routes.length - 1)?.name === route.name) { + const previousRoute = fullState.routes.at(-2); + + // Skip if this is not a fullscreen navigator or if we're already inside the same navigator + if (!isFullScreenName(route.name) || previousRoute?.name === route.name) { return route; } - // Find an already mounted route with the same navigator name + // Look for an already mounted navigator with the same name but a different key const existingRoute = fullState.routes.find((r) => r.name === route.name && r.key !== route.key); if (!existingRoute) { return route; } - // Avoid duplicate keys in the rendered list. + // Prevent rendering two routes with the same key if (routesToRender.some((r) => r.key === existingRoute.key)) { return route; } - // Transfer animation marker so the entering animation fires on the reused component. + // Move the entering-animation marker to the reused route key + // so the animation runs on the existing navigator instance if (screensWithEnteringAnimation.has(route.key)) { screensWithEnteringAnimation.delete(route.key); screensWithEnteringAnimation.add(existingRoute.key); } return { - ...existingRoute, // Preserve key and internal navigator state + ...existingRoute, // Reuse the mounted navigator (preserve key and internal state) params: { - ...route.params, // Apply params from the new navigation action + ...route.params, // Apply params from the incoming navigation action }, }; }); From c79f24e9d2dde31929def887d4cec1d54b3bbb54 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 2 Mar 2026 13:18:51 +0100 Subject: [PATCH 15/27] fix prettier and eslint, remove unnecessary hook from split navigator --- .../RootStackRouter.ts | 1 - .../createRootStackNavigator/index.tsx | 6 +-- .../useCustomRootStackNavigatorState/index.ts | 2 - .../reuseNavigatorKey.ts | 6 +-- .../createSplitNavigator/index.tsx | 2 - .../useNavigateOnParamsChange.ts | 46 +++++++++++++++++++ src/libs/Navigation/Navigation.ts | 1 - 7 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts index c1fe00c1dfa0..4f57fa322570 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts @@ -116,7 +116,6 @@ function RootStackRouter(options: RootStackNavigatorRouterOptions) { return { ...stackRouter, getStateForAction(state: StackNavigationState, action: RootStackNavigatorAction, configOptions: RouterConfigOptions) { - // Evaluate navigation guards FIRST const guardState = handleNavigationGuards(state, action, configOptions, stackRouter); if (guardState) { diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx index af6a121cd86f..b6138ecd540c 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx @@ -1,10 +1,10 @@ -import type { NavigationProp, NavigatorTypeBagBase, ParamListBase, StaticConfig, TypedNavigator } from '@react-navigation/native'; -import { createNavigatorFactory } from '@react-navigation/native'; +import type {NavigationProp, NavigatorTypeBagBase, ParamListBase, StaticConfig, TypedNavigator} from '@react-navigation/native'; +import {createNavigatorFactory} from '@react-navigation/native'; import RootNavigatorExtraContent from '@components/Navigation/RootNavigatorExtraContent'; import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange'; import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent'; import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions'; -import type { PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState } from '@libs/Navigation/PlatformStackNavigation/types'; +import type {PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types'; import RootStackRouter from './RootStackRouter'; import useCustomRootStackNavigatorState from './useCustomRootStackNavigatorState'; diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts index 320de6c9a9f9..2ae33c9b8f18 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts @@ -18,7 +18,5 @@ export default function useCustomRootStackNavigatorState({state}: CustomStateHoo const routesToRender = state.routes.slice(indexToSlice, state.routes.length); const remappedRoutes = reuseNavigatorKey(routesToRender, state); - debugger; - return {...state, routes: remappedRoutes, index: remappedRoutes.length - 1}; } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index dac2390e5b9a..d79fc67c93b2 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -1,6 +1,6 @@ -import type { ParamListBase, StackNavigationState } from '@react-navigation/native'; -import { screensWithEnteringAnimation } from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; -import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; +import type {ParamListBase, StackNavigationState} from '@react-navigation/native'; +import {screensWithEnteringAnimation} from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; +import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; type StateRoutes = StackNavigationState['routes']; diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx index 03b6d0f4a6bd..4483297f6a1b 100644 --- a/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx @@ -13,13 +13,11 @@ import type { } from '@libs/Navigation/PlatformStackNavigation/types'; import SidebarSpacerWrapper from './SidebarSpacerWrapper'; import SplitRouter from './SplitRouter'; -import useNavigateOnParamsChange from './useNavigateOnParamsChange'; import usePreserveNavigatorState from './usePreserveNavigatorState'; function useCustomEffects(props: CustomEffectsHookProps) { useNavigationResetOnLayoutChange(props); usePreserveNavigatorState(props.state, props.parentRoute); - useNavigateOnParamsChange(props); } function useCustomSplitNavigatorState({state}: CustomStateHookProps) { diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts new file mode 100644 index 000000000000..1e7eb1b5bd4d --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts @@ -0,0 +1,46 @@ +import {useEffect, useRef} from 'react'; +import type {CustomEffectsHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; + +/** + * When reuseNavigatorKey remaps a new route to reuse an existing navigator component, + * the component keeps its old internal navigation state but receives new parentRoute.params + * pointing to a different screen. This hook detects that change and dispatches a navigate + * action within the split navigator so the correct screen is shown. + * + * Without this, clicking a sub-tab (e.g. wallet) while already inside a split navigator + * would change the URL but leave the displayed screen unchanged. + */ +function useNavigateOnParamsChange({navigation, parentRoute, state}: CustomEffectsHookProps) { + // Initialize with current params so we don't navigate on the first mount. + const lastHandledParamsRef = useRef(parentRoute?.params); + + // No dependency array — runs after every render. + // The ref guard ensures we only act when params actually change. + useEffect(() => { + const currentParams = parentRoute?.params; + + if (lastHandledParamsRef.current === currentParams) { + return; + } + + lastHandledParamsRef.current = currentParams; + + const screenToNavigate = currentParams && 'screen' in currentParams ? (currentParams.screen as string) : undefined; + + if (!screenToNavigate) { + return; + } + + // Already showing the correct screen — nothing to do. + const lastRoute = state.routes.at(-1); + if (lastRoute?.name === screenToNavigate) { + return; + } + + const screenParams = currentParams && 'params' in currentParams ? (currentParams.params as Record) : undefined; + + navigation.navigate(screenToNavigate, screenParams); + }); +} + +export default useNavigateOnParamsChange; diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 8009846422ef..6026d5a488bf 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -487,7 +487,6 @@ function goBack(backToRoute?: Route, options?: GoBackOptions) { * see the NAVIGATION.md documentation. */ function popToSidebar() { - setShouldPopToSidebar(false); const rootState = navigationRef.current?.getRootState(); From 1a9196856959ddf806f33aafdbcc70db0718a57b Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 2 Mar 2026 13:23:07 +0100 Subject: [PATCH 16/27] remove changes from jest test --- tests/navigation/NavigateTests.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/navigation/NavigateTests.tsx b/tests/navigation/NavigateTests.tsx index 008cec2fd979..636b5995178e 100644 --- a/tests/navigation/NavigateTests.tsx +++ b/tests/navigation/NavigateTests.tsx @@ -210,7 +210,6 @@ describe('Navigate', () => { name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, state: { index: 0, - stale: false, routes: [ { name: SCREENS.SETTINGS.ROOT, From 3863f2ae1f0048998442c6f79e1bfad4722fa33f Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Fri, 6 Mar 2026 16:57:47 +0100 Subject: [PATCH 17/27] block pushing duplicate of split navigator --- .../reuseNavigatorKey.ts | 32 +++++++++++++++++-- .../createSplitNavigator/SplitRouter.ts | 7 ++-- src/libs/Navigation/NavigationRoot.tsx | 3 ++ .../helpers/linkTo/getMinimalAction.ts | 13 ++++++-- src/libs/Navigation/helpers/linkTo/index.ts | 11 ++++++- 5 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index d79fc67c93b2..63a4da9aa47f 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -4,6 +4,21 @@ import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; type StateRoutes = StackNavigationState['routes']; +type RemappedStateRoute = StateRoutes[number] & {originalKey?: string}; + +/** + * Tracks the mapping from a new route's key (originalKey) to the mounted navigator's key. + * This allows getMinimalAction to drill into the mounted navigator's preserved state + * even though the new route has no embedded state in the root navigation state. + * + * Example: when T3unFG is remapped to MBc.key, remappedKeyMap[T3unFGKey] = MBcKey. + */ +const remappedKeyMap: Record = {}; + +function getRemappedNavigatorKey(originalKey: string): string | undefined { + return remappedKeyMap[originalKey]; +} + /** * For each fullscreen navigator in routesToRender, checks whether a navigator * with the same name is already mounted in the current navigation state. @@ -16,7 +31,12 @@ type StateRoutes = StackNavigationState['routes']; * * The actual navigation state is not mutated — this only affects what gets rendered. */ -function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigationState): StateRoutes { +function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigationState): RemappedStateRoute[] { + // Rebuild the mapping from scratch on every call so it always reflects the current render state. + for (const key of Object.keys(remappedKeyMap)) { + delete remappedKeyMap[key]; + } + return routesToRender.map((route) => { const previousRoute = fullState.routes.at(-2); @@ -44,13 +64,21 @@ function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigati screensWithEnteringAnimation.add(existingRoute.key); } + // Track that route.key is rendered via existingRoute.key so that getMinimalAction + // can look up the mounted navigator's preserved state when route has no embedded state. + remappedKeyMap[route.key] = existingRoute.key; + return { - ...existingRoute, // Reuse the mounted navigator (preserve key and internal state) + ...existingRoute, // Reuse the mounted navigator (preserve key so React doesn't unmount it) + state: undefined, // Clear old embedded state so getInitialState is called with the new params params: { ...route.params, // Apply params from the incoming navigation action }, + originalKey: route.key, }; }); } +export type {RemappedStateRoute}; +export {getRemappedNavigatorKey}; export default reuseNavigatorKey; diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts index 397388f73693..2754a5f6dd8c 100644 --- a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts @@ -75,7 +75,8 @@ function adaptStateIfNecessary({state, options: {sidebarScreen, defaultCentralSc const previousSameNavigator = rootState?.routes.filter((route) => route.name === parentRoute.name).at(-2); // If we have optimization for not rendering all split navigators, then last selected option may not be in the state. In this case state has to be read from the preserved state. - const previousSameNavigatorState = previousSameNavigator?.state ?? (previousSameNavigator?.key ? getPreservedNavigatorState(previousSameNavigator.key) : undefined); + const previousSameNavigatorKey = (previousSameNavigator as {originalKey?: string} | undefined)?.originalKey ?? previousSameNavigator?.key; + const previousSameNavigatorState = previousSameNavigator?.state ?? (previousSameNavigatorKey ? getPreservedNavigatorState(previousSameNavigatorKey) : undefined); const previousSelectedCentralScreen = previousSameNavigatorState?.routes && previousSameNavigatorState.routes.length > 1 ? previousSameNavigatorState.routes.at(-1)?.name : undefined; @@ -136,7 +137,9 @@ function SplitRouter(options: SplitNavigatorRouterOptions) { }, getInitialState({routeNames, routeParamList, routeGetIdList}: RouterConfigOptions) { - const initialState = getPreservedNavigatorState(options.parentRoute.key) ?? stackRouter.getInitialState({routeNames, routeParamList, routeGetIdList}); + const parentRouteKey: string = (options.parentRoute as {originalKey?: string}).originalKey ?? options.parentRoute.key; + debugger; + const initialState = getPreservedNavigatorState(parentRouteKey) ?? stackRouter.getInitialState({routeNames, routeParamList, routeGetIdList}); const maybeAdaptedState = adaptStateIfNecessary({state: initialState, options}); // If we needed to modify the state we need to rehydrate it to get keys for new routes. diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 2844878af164..788e4a567b1f 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -240,6 +240,9 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N // We want to clean saved scroll offsets for screens that aren't anymore in the state. cleanStaleScrollOffsets(state); cleanPreservedNavigatorStates(state); + + console.log("dupa") + console.log(state) }; const onReadyWithSentry = useCallback(() => { diff --git a/src/libs/Navigation/helpers/linkTo/getMinimalAction.ts b/src/libs/Navigation/helpers/linkTo/getMinimalAction.ts index 9eab2f6f8717..f7d56d884256 100644 --- a/src/libs/Navigation/helpers/linkTo/getMinimalAction.ts +++ b/src/libs/Navigation/helpers/linkTo/getMinimalAction.ts @@ -13,19 +13,26 @@ type MinimalAction = { * * @param action action generated by getActionFromState * @param state The root state + * @param getStateForRouteKey optional fallback to look up a navigator's state by route key + * when the route has no embedded state in the root state (e.g. because it is rendered + * via a remapped key from reuseNavigatorKey and never mounted under its own key). * @returns minimalAction minimal action is the action that we should dispatch */ -function getMinimalAction(action: NavigationAction, state: NavigationState): MinimalAction { +function getMinimalAction(action: NavigationAction, state: NavigationState, getStateForRouteKey?: (key: string) => State | undefined): MinimalAction { let currentAction: NavigationAction = action; let currentState: State | undefined = state; let currentTargetKey: string | undefined; while (currentAction.payload && 'name' in currentAction.payload && currentState?.routes[currentState.index ?? -1].name === currentAction.payload.name) { - if (!currentState?.routes[currentState.index ?? -1].state) { + // Cast to a known shape so TypeScript can type .key and .state without falling back to any. + const currentRoute = currentState?.routes[currentState.index ?? -1] as {key: string; state?: State} | undefined; + const routeState = currentRoute?.state ?? getStateForRouteKey?.(currentRoute?.key ?? ''); + + if (!routeState) { break; } - currentState = currentState?.routes[currentState.index ?? -1].state; + currentState = routeState; currentTargetKey = currentState?.key; const payload = currentAction.payload as ActionPayload; diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 63126c40e1c7..47077f0b6533 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -1,6 +1,8 @@ import {getActionFromState} from '@react-navigation/core'; import type {NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native'; import {findFocusedRoute, StackActions} from '@react-navigation/native'; +import {getRemappedNavigatorKey} from '@libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey'; +import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState'; import {getMatchingFullScreenRoute, isFullScreenName} from '@libs/Navigation/helpers/getAdaptedStateFromPath'; import getStateFromPath from '@libs/Navigation/helpers/getStateFromPath'; import normalizePath from '@libs/Navigation/helpers/normalizePath'; @@ -182,6 +184,13 @@ export default function linkTo(navigation: NavigationContainerRef { + const mountedKey = getRemappedNavigatorKey(key) ?? key; + return getPreservedNavigatorState(mountedKey); + }; + + const {action: minimalAction} = getMinimalAction(action, navigation.getRootState(), getStateForRouteKey); navigation.dispatch(minimalAction); } From a61eadf8b013d5441170e2847580299649cf5f04 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 9 Mar 2026 10:01:32 +0100 Subject: [PATCH 18/27] refactor code --- .../GetStateForActionHandlers.ts | 59 ++++++++++++++++--- .../RootStackRouter.ts | 1 + .../index.android.ts | 4 +- .../index.ios.ts | 4 +- .../useCustomRootStackNavigatorState/index.ts | 4 +- .../reuseNavigatorKey.ts | 31 +++++++--- .../createSplitNavigator/SplitRouter.ts | 1 - .../useNavigateOnParamsChange.ts | 2 +- src/libs/Navigation/NavigationRoot.tsx | 3 - .../helpers/linkTo/getMinimalAction.ts | 2 +- src/libs/Navigation/helpers/linkTo/index.ts | 2 +- 11 files changed, 84 insertions(+), 29 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index 52a53673ec17..fba96f0202c4 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -1,15 +1,57 @@ -import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; -import {StackActions} from '@react-navigation/native'; -import type {ParamListBase, Router} from '@react-navigation/routers'; +import type { CommonActions, RouterConfigOptions, StackActionType, StackNavigationState } from '@react-navigation/native'; +import { StackActions } from '@react-navigation/native'; +import type { ParamListBase, Router } from '@react-navigation/routers'; import SCREENS_WITH_NAVIGATION_TAB_BAR from '@components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Log from '@libs/Log'; -import {isSplitNavigatorName} from '@libs/Navigation/helpers/isNavigatorName'; -import {SPLIT_TO_SIDEBAR} from '@libs/Navigation/linkingConfig/RELATIONS'; +import { isSplitNavigatorName } from '@libs/Navigation/helpers/isNavigatorName'; +import { SPLIT_TO_SIDEBAR } from '@libs/Navigation/linkingConfig/RELATIONS'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; -import type {OpenDomainSplitActionType, OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType} from './types'; +import type { OpenDomainSplitActionType, OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType } from './types'; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + const MODAL_ROUTES_TO_DISMISS = new Set([ NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, @@ -120,9 +162,10 @@ function handlePushFullscreenAction( configOptions: RouterConfigOptions, stackRouter: Router, CommonActions.Action | StackActionType>, ) { - const navigatorName = action.payload.name; const targetScreen = action.payload?.params && 'screen' in action.payload.params ? (action.payload?.params?.screen as string) : undefined; + const navigatorName = action.payload.name; + // If we navigate to the central screen of the split navigator, we need to filter this navigator from preloadedRoutes to remove a sidebar screen from the state const shouldFilterPreloadedRoutes = getIsNarrowLayout() && isSplitNavigatorName(navigatorName) && @@ -130,7 +173,6 @@ function handlePushFullscreenAction( state.preloadedRoutes?.some((preloadedRoute) => preloadedRoute.name === navigatorName); const adjustedState = shouldFilterPreloadedRoutes ? {...state, preloadedRoutes: state.preloadedRoutes.filter((preloadedRoute) => preloadedRoute.name !== navigatorName)} : state; - const stateWithNavigator = stackRouter.getStateForAction(adjustedState, action, configOptions); if (!stateWithNavigator) { @@ -138,6 +180,7 @@ function handlePushFullscreenAction( return null; } + // Transitioning to all central screens in each split should be animated const lastFullScreenRoute = stateWithNavigator.routes.at(-1); if (lastFullScreenRoute?.key && targetScreen && !SCREENS_WITH_NAVIGATION_TAB_BAR.includes(targetScreen)) { screensWithEnteringAnimation.add(lastFullScreenRoute.key); diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts index 4f57fa322570..8eceff88a361 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts @@ -113,6 +113,7 @@ function isNavigatingToModalFromModal(state: StackNavigationState function RootStackRouter(options: RootStackNavigatorRouterOptions) { const stackRouter = StackRouter(options); + return { ...stackRouter, getStateForAction(state: StackNavigationState, action: RootStackNavigatorAction, configOptions: RouterConfigOptions) { diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.android.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.android.ts index c060873e981f..789a412b9bb3 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.android.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.android.ts @@ -1,6 +1,6 @@ import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import reuseNavigatorKey from './reuseNavigatorKey'; +import buildOptimizedRoutes from './reuseNavigatorKey'; // This is an optimization to keep mounted only last few screens in the stack. export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { @@ -9,6 +9,6 @@ export default function useCustomRootStackNavigatorState({state}: CustomStateHoo // Always preserve the previous fullscreen route so React can reuse the mounted navigator component on navigation. const indexToSlice = Math.max(0, lastSplitIndex - 1); const routesToRender = state.routes.slice(indexToSlice, state.routes.length); - const remappedRoutes = reuseNavigatorKey(routesToRender, state); + const remappedRoutes = buildOptimizedRoutes(routesToRender, state); return {...state, routes: remappedRoutes, index: remappedRoutes.length - 1}; } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ios.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ios.ts index b23fffcb7893..ae5a7f2a85e5 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ios.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ios.ts @@ -4,7 +4,7 @@ import {SPLIT_TO_SIDEBAR} from '@libs/Navigation/linkingConfig/RELATIONS'; import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {NavigationRoute, SplitNavigatorName} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; -import reuseNavigatorKey from './reuseNavigatorKey'; +import buildOptimizedRoutes from './reuseNavigatorKey'; // Swiping back on iOS does not work properly when the preloaded route has gestureEnabled set to false. // Therefore, on screens where swiping should work, preloadedRoutes will be an empty array during rendering to ensure swiping works properly. @@ -37,7 +37,7 @@ function getShouldHidePreloadedRoutes(route?: NavigationRoute) { export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); const routesToRender = state.routes.slice(Math.max(0, lastSplitIndex - 1), state.routes.length); - const remappedRoutes = reuseNavigatorKey(routesToRender, state); + const remappedRoutes = buildOptimizedRoutes(routesToRender, state); const stateToRender = {...state, routes: remappedRoutes, index: remappedRoutes.length - 1}; if (getShouldHidePreloadedRoutes(stateToRender.routes.at(-1))) { return {...stateToRender, preloadedRoutes: []}; diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts index 2ae33c9b8f18..ec5fec7ed29b 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts @@ -1,6 +1,6 @@ import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import reuseNavigatorKey from './reuseNavigatorKey'; +import buildOptimizedRoutes from './reuseNavigatorKey'; // This is an optimization to keep mounted only last few screens in the stack. export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { @@ -16,7 +16,7 @@ export default function useCustomRootStackNavigatorState({state}: CustomStateHoo } const routesToRender = state.routes.slice(indexToSlice, state.routes.length); - const remappedRoutes = reuseNavigatorKey(routesToRender, state); + const remappedRoutes = buildOptimizedRoutes(routesToRender, state); return {...state, routes: remappedRoutes, index: remappedRoutes.length - 1}; } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index 63a4da9aa47f..dc04d8c68552 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -1,6 +1,21 @@ -import type {ParamListBase, StackNavigationState} from '@react-navigation/native'; -import {screensWithEnteringAnimation} from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; -import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; +import type { ParamListBase, StackNavigationState } from '@react-navigation/native'; +import { screensWithEnteringAnimation } from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; +import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; + + + + + + + + + + + + + + + type StateRoutes = StackNavigationState['routes']; @@ -11,7 +26,7 @@ type RemappedStateRoute = StateRoutes[number] & {originalKey?: string}; * This allows getMinimalAction to drill into the mounted navigator's preserved state * even though the new route has no embedded state in the root navigation state. * - * Example: when T3unFG is remapped to MBc.key, remappedKeyMap[T3unFGKey] = MBcKey. + * Example: when Inbox.key = key1 is remapped to Inbox.key = key2, remappedKeyMap[key2] = key1. */ const remappedKeyMap: Record = {}; @@ -31,14 +46,14 @@ function getRemappedNavigatorKey(originalKey: string): string | undefined { * * The actual navigation state is not mutated — this only affects what gets rendered. */ -function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigationState): RemappedStateRoute[] { +function buildOptimizedRoutes(routesToRender: StateRoutes, state: StackNavigationState): RemappedStateRoute[] { // Rebuild the mapping from scratch on every call so it always reflects the current render state. for (const key of Object.keys(remappedKeyMap)) { delete remappedKeyMap[key]; } return routesToRender.map((route) => { - const previousRoute = fullState.routes.at(-2); + const previousRoute = state.routes.at(-2); // Skip if this is not a fullscreen navigator or if we're already inside the same navigator if (!isFullScreenName(route.name) || previousRoute?.name === route.name) { @@ -46,7 +61,7 @@ function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigati } // Look for an already mounted navigator with the same name but a different key - const existingRoute = fullState.routes.find((r) => r.name === route.name && r.key !== route.key); + const existingRoute = state.routes.find((r) => r.name === route.name && r.key !== route.key); if (!existingRoute) { return route; @@ -81,4 +96,4 @@ function reuseNavigatorKey(routesToRender: StateRoutes, fullState: StackNavigati export type {RemappedStateRoute}; export {getRemappedNavigatorKey}; -export default reuseNavigatorKey; +export default buildOptimizedRoutes; diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts index 2754a5f6dd8c..7cc4b0dd4a1a 100644 --- a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts @@ -138,7 +138,6 @@ function SplitRouter(options: SplitNavigatorRouterOptions) { getInitialState({routeNames, routeParamList, routeGetIdList}: RouterConfigOptions) { const parentRouteKey: string = (options.parentRoute as {originalKey?: string}).originalKey ?? options.parentRoute.key; - debugger; const initialState = getPreservedNavigatorState(parentRouteKey) ?? stackRouter.getInitialState({routeNames, routeParamList, routeGetIdList}); const maybeAdaptedState = adaptStateIfNecessary({state: initialState, options}); diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts index 1e7eb1b5bd4d..66631f627200 100644 --- a/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts @@ -2,7 +2,7 @@ import {useEffect, useRef} from 'react'; import type {CustomEffectsHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; /** - * When reuseNavigatorKey remaps a new route to reuse an existing navigator component, + * When buildOptimizedRoutes remaps a new route to reuse an existing navigator component, * the component keeps its old internal navigation state but receives new parentRoute.params * pointing to a different screen. This hook detects that change and dispatches a navigate * action within the split navigator so the correct screen is shown. diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 788e4a567b1f..2844878af164 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -240,9 +240,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N // We want to clean saved scroll offsets for screens that aren't anymore in the state. cleanStaleScrollOffsets(state); cleanPreservedNavigatorStates(state); - - console.log("dupa") - console.log(state) }; const onReadyWithSentry = useCallback(() => { diff --git a/src/libs/Navigation/helpers/linkTo/getMinimalAction.ts b/src/libs/Navigation/helpers/linkTo/getMinimalAction.ts index f7d56d884256..28f1fde85fdb 100644 --- a/src/libs/Navigation/helpers/linkTo/getMinimalAction.ts +++ b/src/libs/Navigation/helpers/linkTo/getMinimalAction.ts @@ -15,7 +15,7 @@ type MinimalAction = { * @param state The root state * @param getStateForRouteKey optional fallback to look up a navigator's state by route key * when the route has no embedded state in the root state (e.g. because it is rendered - * via a remapped key from reuseNavigatorKey and never mounted under its own key). + * via a remapped key from buildOptimizedRoutes and never mounted under its own key). * @returns minimalAction minimal action is the action that we should dispatch */ function getMinimalAction(action: NavigationAction, state: NavigationState, getStateForRouteKey?: (key: string) => State | undefined): MinimalAction { diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 47077f0b6533..c84e27a0a4a2 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -184,7 +184,7 @@ export default function linkTo(navigation: NavigationContainerRef { const mountedKey = getRemappedNavigatorKey(key) ?? key; From 6d617247e66497aed273b14f147ef2ed79b74155 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 9 Mar 2026 10:13:11 +0100 Subject: [PATCH 19/27] npm run prettier --- .../GetStateForActionHandlers.ts | 54 +++---------------- .../reuseNavigatorKey.ts | 21 ++------ 2 files changed, 9 insertions(+), 66 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts index fba96f0202c4..f4833a3281c6 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts @@ -1,57 +1,15 @@ -import type { CommonActions, RouterConfigOptions, StackActionType, StackNavigationState } from '@react-navigation/native'; -import { StackActions } from '@react-navigation/native'; -import type { ParamListBase, Router } from '@react-navigation/routers'; +import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; +import {StackActions} from '@react-navigation/native'; +import type {ParamListBase, Router} from '@react-navigation/routers'; import SCREENS_WITH_NAVIGATION_TAB_BAR from '@components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Log from '@libs/Log'; -import { isSplitNavigatorName } from '@libs/Navigation/helpers/isNavigatorName'; -import { SPLIT_TO_SIDEBAR } from '@libs/Navigation/linkingConfig/RELATIONS'; +import {isSplitNavigatorName} from '@libs/Navigation/helpers/isNavigatorName'; +import {SPLIT_TO_SIDEBAR} from '@libs/Navigation/linkingConfig/RELATIONS'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; -import type { OpenDomainSplitActionType, OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType } from './types'; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +import type {OpenDomainSplitActionType, OpenWorkspaceSplitActionType, PushActionType, ReplaceActionType, ToggleSidePanelWithHistoryActionType} from './types'; const MODAL_ROUTES_TO_DISMISS = new Set([ NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index dc04d8c68552..181ec46f6f6e 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -1,21 +1,6 @@ -import type { ParamListBase, StackNavigationState } from '@react-navigation/native'; -import { screensWithEnteringAnimation } from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; -import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; - - - - - - - - - - - - - - - +import type {ParamListBase, StackNavigationState} from '@react-navigation/native'; +import {screensWithEnteringAnimation} from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers'; +import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; type StateRoutes = StackNavigationState['routes']; From 165359e32bdec134672e054e6afdd0764c8833a0 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 9 Mar 2026 13:19:15 +0100 Subject: [PATCH 20/27] fix empty tab on mobile ios --- .../useCustomRootStackNavigatorState/index.ts | 13 +++++++++++-- .../reuseNavigatorKey.ts | 13 +++++++++++++ src/libs/Navigation/NavigationRoot.tsx | 3 +++ src/libs/Navigation/navigationRef.ts | 2 ++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts index ec5fec7ed29b..d9e0af277088 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts @@ -1,7 +1,12 @@ -import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; -import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; +import type { CustomStateHookProps } from '@libs/Navigation/PlatformStackNavigation/types'; import buildOptimizedRoutes from './reuseNavigatorKey'; + + + + + // This is an optimization to keep mounted only last few screens in the stack. export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); @@ -18,5 +23,9 @@ export default function useCustomRootStackNavigatorState({state}: CustomStateHoo const routesToRender = state.routes.slice(indexToSlice, state.routes.length); const remappedRoutes = buildOptimizedRoutes(routesToRender, state); + + console.log("to render") + console.log({...state, routes: remappedRoutes, index: remappedRoutes.length - 1}); + return {...state, routes: remappedRoutes, index: remappedRoutes.length - 1}; } diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index 181ec46f6f6e..b527c4aafa68 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -57,6 +57,19 @@ function buildOptimizedRoutes(routesToRender: StateRoutes, state: StackNavigatio return route; } + // Prevent reordering of native views: skip remap if there are routes between existingRoute + // and the current route in the full state that are also being rendered. Remapping in this + // case would swap their positions in the array and cause a blank screen in the native stack. + const existingRouteIndexInFull = state.routes.findIndex((r) => r.key === existingRoute.key); + const currentRouteIndexInFull = state.routes.findIndex((r) => r.key === route.key); + const hasIntermediateRenderedRoutes = state.routes + .slice(existingRouteIndexInFull + 1, currentRouteIndexInFull) + .some((routeBetween) => routesToRender.some((r) => r.key === routeBetween.key)); + + if (hasIntermediateRenderedRoutes) { + return route; + } + // Move the entering-animation marker to the reused route key // so the animation runs on the existing navigator instance if (screensWithEnteringAnimation.has(route.key)) { diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 2844878af164..519a492aa6d0 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -240,6 +240,9 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N // We want to clean saved scroll offsets for screens that aren't anymore in the state. cleanStaleScrollOffsets(state); cleanPreservedNavigatorStates(state); + + console.log("dupa dupa") + console.log(state) }; const onReadyWithSentry = useCallback(() => { diff --git a/src/libs/Navigation/navigationRef.ts b/src/libs/Navigation/navigationRef.ts index 17742e5d0937..87adf1c246ee 100644 --- a/src/libs/Navigation/navigationRef.ts +++ b/src/libs/Navigation/navigationRef.ts @@ -3,4 +3,6 @@ import type {NavigationRef} from './types'; const navigationRef: NavigationRef = createNavigationContainerRef(); +// document.navigationRef = navigationRef; + export default navigationRef; From 8419336a110b62af72adcf26501c15471fcc2583 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 9 Mar 2026 13:24:21 +0100 Subject: [PATCH 21/27] fix prettier --- .../useCustomRootStackNavigatorState/index.ts | 12 +++--------- src/libs/Navigation/NavigationRoot.tsx | 3 --- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts index d9e0af277088..e536b0ea817c 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts @@ -1,12 +1,7 @@ -import { isFullScreenName } from '@libs/Navigation/helpers/isNavigatorName'; -import type { CustomStateHookProps } from '@libs/Navigation/PlatformStackNavigation/types'; +import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; +import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; import buildOptimizedRoutes from './reuseNavigatorKey'; - - - - - // This is an optimization to keep mounted only last few screens in the stack. export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) { const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); @@ -23,8 +18,7 @@ export default function useCustomRootStackNavigatorState({state}: CustomStateHoo const routesToRender = state.routes.slice(indexToSlice, state.routes.length); const remappedRoutes = buildOptimizedRoutes(routesToRender, state); - - console.log("to render") + console.log('to render'); console.log({...state, routes: remappedRoutes, index: remappedRoutes.length - 1}); return {...state, routes: remappedRoutes, index: remappedRoutes.length - 1}; diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 519a492aa6d0..2844878af164 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -240,9 +240,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N // We want to clean saved scroll offsets for screens that aren't anymore in the state. cleanStaleScrollOffsets(state); cleanPreservedNavigatorStates(state); - - console.log("dupa dupa") - console.log(state) }; const onReadyWithSentry = useCallback(() => { From d9d0fc69c7b448abe2f13cb5a9ad57e693bf9a6c Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Tue, 10 Mar 2026 11:33:22 +0100 Subject: [PATCH 22/27] wip --- src/libs/Navigation/NavigationRoot.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 2844878af164..862dd9fdf792 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -240,6 +240,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N // We want to clean saved scroll offsets for screens that aren't anymore in the state. cleanStaleScrollOffsets(state); cleanPreservedNavigatorStates(state); + + console.log('Navigation state changed', state); }; const onReadyWithSentry = useCallback(() => { From 3049e5901509931eed034c8cce4748936b659446 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Tue, 10 Mar 2026 11:33:27 +0100 Subject: [PATCH 23/27] wip --- .../reuseNavigatorKey.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index b527c4aafa68..907d800ddf6f 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -9,7 +9,7 @@ type RemappedStateRoute = StateRoutes[number] & {originalKey?: string}; /** * Tracks the mapping from a new route's key (originalKey) to the mounted navigator's key. * This allows getMinimalAction to drill into the mounted navigator's preserved state - * even though the new route has no embedded state in the root navigation state. + * when looking up the nested state for a route that is rendered via a remapped key. * * Example: when Inbox.key = key1 is remapped to Inbox.key = key2, remappedKeyMap[key2] = key1. */ @@ -27,7 +27,9 @@ function getRemappedNavigatorKey(originalKey: string): string | undefined { * with the existing one while applying the new params. * * This preserves the original route key so React can reuse the already-mounted - * navigator instance, while the updated params instruct it which screen to display. + * navigator instance. The existing navigator's internal state is kept intact so its + * navigation history is not lost. useNavigateOnParamsChange will forward-navigate + * to the correct screen based on the updated params. * * The actual navigation state is not mutated — this only affects what gets rendered. */ @@ -78,12 +80,14 @@ function buildOptimizedRoutes(routesToRender: StateRoutes, state: StackNavigatio } // Track that route.key is rendered via existingRoute.key so that getMinimalAction - // can look up the mounted navigator's preserved state when route has no embedded state. + // can look up the mounted navigator's preserved state. remappedKeyMap[route.key] = existingRoute.key; return { ...existingRoute, // Reuse the mounted navigator (preserve key so React doesn't unmount it) - state: undefined, // Clear old embedded state so getInitialState is called with the new params + // Do not clear state — keeping the existing navigator's internal state preserves its + // navigation history. useNavigateOnParamsChange will forward-navigate to the correct + // screen based on the updated params, so history is extended rather than reset. params: { ...route.params, // Apply params from the incoming navigation action }, From 9dfa35653c6147e8aae44c27c7acad1a017eb001 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Wed, 11 Mar 2026 12:36:02 +0100 Subject: [PATCH 24/27] resolve state problem --- .../reuseNavigatorKey.ts | 19 +++++++++++++------ .../useNavigateOnParamsChange.ts | 15 +++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index 907d800ddf6f..0e12b31b96c9 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -59,16 +59,23 @@ function buildOptimizedRoutes(routesToRender: StateRoutes, state: StackNavigatio return route; } + const existingRouteIndexInFull = state.routes.findIndex((r) => r.key === existingRoute.key); + const currentRouteIndexInFull = state.routes.findIndex((r) => r.key === route.key); + const routesBetween = state.routes.slice(existingRouteIndexInFull + 1, currentRouteIndexInFull); + // Prevent reordering of native views: skip remap if there are routes between existingRoute // and the current route in the full state that are also being rendered. Remapping in this // case would swap their positions in the array and cause a blank screen in the native stack. - const existingRouteIndexInFull = state.routes.findIndex((r) => r.key === existingRoute.key); - const currentRouteIndexInFull = state.routes.findIndex((r) => r.key === route.key); - const hasIntermediateRenderedRoutes = state.routes - .slice(existingRouteIndexInFull + 1, currentRouteIndexInFull) - .some((routeBetween) => routesToRender.some((r) => r.key === routeBetween.key)); + const hasIntermediateRenderedRoutes = routesBetween.some((routeBetween) => routesToRender.some((r) => r.key === routeBetween.key)); + + // Skip remap if there are routes of a different navigator type between existingRoute and + // currentRoute in the full navigation state. Without this guard, on platforms that only + // render one route at a time (web), the current route would be rendered under existingRoute's + // key and never mounted under its own key — so it would never receive embedded state, + // which breaks the URL and back navigation. + const hasIntermediateRoutesOfDifferentType = routesBetween.some((routeBetween) => routeBetween.name !== route.name); - if (hasIntermediateRenderedRoutes) { + if (hasIntermediateRenderedRoutes || hasIntermediateRoutesOfDifferentType) { return route; } diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts index 66631f627200..a301cf4826b4 100644 --- a/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts @@ -1,3 +1,4 @@ +import isEqual from 'lodash/isEqual'; import {useEffect, useRef} from 'react'; import type {CustomEffectsHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -11,8 +12,10 @@ import type {CustomEffectsHookProps} from '@libs/Navigation/PlatformStackNavigat * would change the URL but leave the displayed screen unchanged. */ function useNavigateOnParamsChange({navigation, parentRoute, state}: CustomEffectsHookProps) { - // Initialize with current params so we don't navigate on the first mount. - const lastHandledParamsRef = useRef(parentRoute?.params); + // Initialized to undefined (not the current params) so that the first render also checks + // whether the navigator's initial state matches the target screen. This is required for + // freshly-mounted navigators whose getInitialState may not navigate to the correct screen. + const lastHandledParamsRef = useRef | undefined>(undefined); // No dependency array — runs after every render. // The ref guard ensures we only act when params actually change. @@ -31,14 +34,14 @@ function useNavigateOnParamsChange({navigation, parentRoute, state}: CustomEffec return; } - // Already showing the correct screen — nothing to do. + const screenParams = currentParams && 'params' in currentParams ? (currentParams.params as Record) : undefined; + + // Already showing the correct screen with the same params — nothing to do. const lastRoute = state.routes.at(-1); - if (lastRoute?.name === screenToNavigate) { + if (lastRoute?.name === screenToNavigate && isEqual(lastRoute?.params, screenParams)) { return; } - const screenParams = currentParams && 'params' in currentParams ? (currentParams.params as Record) : undefined; - navigation.navigate(screenToNavigate, screenParams); }); } From f8d17422e83cda06e04e0f6c8c8c97ac8a197f47 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Wed, 11 Mar 2026 13:10:29 +0100 Subject: [PATCH 25/27] working on performance --- .../useCustomRootStackNavigatorState/index.ts | 3 -- .../Navigation/helpers/getPathFromState.ts | 53 +++++++++++++++++-- src/libs/Navigation/linkingConfig/index.ts | 2 + 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts index e536b0ea817c..ec5fec7ed29b 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ts @@ -18,8 +18,5 @@ export default function useCustomRootStackNavigatorState({state}: CustomStateHoo const routesToRender = state.routes.slice(indexToSlice, state.routes.length); const remappedRoutes = buildOptimizedRoutes(routesToRender, state); - console.log('to render'); - console.log({...state, routes: remappedRoutes, index: remappedRoutes.length - 1}); - return {...state, routes: remappedRoutes, index: remappedRoutes.length - 1}; } diff --git a/src/libs/Navigation/helpers/getPathFromState.ts b/src/libs/Navigation/helpers/getPathFromState.ts index fa48471b6f8d..09a6de0e096e 100644 --- a/src/libs/Navigation/helpers/getPathFromState.ts +++ b/src/libs/Navigation/helpers/getPathFromState.ts @@ -1,7 +1,7 @@ import {findFocusedRoute, getPathFromState as RNGetPathFromState} from '@react-navigation/native'; import type {NavigationState, PartialState} from '@react-navigation/routers'; -import {linkingConfig} from '@libs/Navigation/linkingConfig'; -import {normalizedConfigs} from '@libs/Navigation/linkingConfig/config'; +import {getRemappedNavigatorKey} from '@libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey'; +import {config, normalizedConfigs} from '@libs/Navigation/linkingConfig/config'; import type {DynamicRouteSuffix} from '@src/ROUTES'; import type {Screen} from '@src/SCREENS'; import {dynamicRoutePaths} from './isDynamicRouteSuffix'; @@ -21,8 +21,53 @@ function isDynamicRouteScreen(screenName: Screen): boolean { return dynamicRoutePaths.has(screenPath as DynamicRouteSuffix); } +/** + * When buildOptimizedRoutes remaps a new route (e.g. RSN-key3) to reuse an existing + * mounted navigator (RSN-key1), the new route is never mounted under its own key and + * its embedded state in the root navigation state remains undefined. This causes + * getPathFromState to serialize route.params directly, producing a broken URL. + * + * This function patches the state by copying the mounted navigator's current state to + * the logical (unmounted) route so URL generation can drill into the correct nested state. + */ +function patchStateWithRemappedStates(state: State): State { + if (!('routes' in state) || !state.routes) { + return state; + } + + let changed = false; + const routes = (state as NavigationState).routes; + + const patchedRoutes = routes.map((route) => { + if (route.state) { + return route; + } + + const remappedKey = getRemappedNavigatorKey(route.key); + if (!remappedKey) { + return route; + } + + const remappedRoute = routes.find((r) => r.key === remappedKey); + if (!remappedRoute?.state) { + return route; + } + + changed = true; + return {...route, state: remappedRoute.state}; + }); + + if (!changed) { + return state; + } + + return {...(state as NavigationState), routes: patchedRoutes}; +} + const getPathFromState = (state: State): string => { - const focusedRoute = findFocusedRoute(state); + const patchedState = patchStateWithRemappedStates(state); + + const focusedRoute = findFocusedRoute(patchedState); const screenName = focusedRoute?.name ?? ''; // Handle dynamic route screens that require special path that is placed in state @@ -31,7 +76,7 @@ const getPathFromState = (state: State): string => { } // For regular routes, use React Navigation's default path generation - const path = RNGetPathFromState(state, linkingConfig.config); + const path = RNGetPathFromState(patchedState, config); return path; }; diff --git a/src/libs/Navigation/linkingConfig/index.ts b/src/libs/Navigation/linkingConfig/index.ts index 4eec394d5bcb..65f20eb3870e 100644 --- a/src/libs/Navigation/linkingConfig/index.ts +++ b/src/libs/Navigation/linkingConfig/index.ts @@ -1,12 +1,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type {LinkingOptions} from '@react-navigation/native'; import getAdaptedStateFromPath from '@libs/Navigation/helpers/getAdaptedStateFromPath'; +import getPathFromState from '@libs/Navigation/helpers/getPathFromState'; import type {RootNavigatorParamList} from '@libs/Navigation/types'; import {config} from './config'; import prefixes from './prefixes'; const linkingConfig: LinkingOptions = { getStateFromPath: getAdaptedStateFromPath, + getPathFromState, prefixes, config, }; From 31dba0b6d5375ccd1253e5ec9cf7e27a5ab156f0 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Wed, 11 Mar 2026 13:21:43 +0100 Subject: [PATCH 26/27] working solution, but not full performance gain --- .../reuseNavigatorKey.ts | 9 +-------- src/libs/Navigation/NavigationRoot.tsx | 2 -- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts index 0e12b31b96c9..b886d865327d 100644 --- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts +++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/reuseNavigatorKey.ts @@ -68,14 +68,7 @@ function buildOptimizedRoutes(routesToRender: StateRoutes, state: StackNavigatio // case would swap their positions in the array and cause a blank screen in the native stack. const hasIntermediateRenderedRoutes = routesBetween.some((routeBetween) => routesToRender.some((r) => r.key === routeBetween.key)); - // Skip remap if there are routes of a different navigator type between existingRoute and - // currentRoute in the full navigation state. Without this guard, on platforms that only - // render one route at a time (web), the current route would be rendered under existingRoute's - // key and never mounted under its own key — so it would never receive embedded state, - // which breaks the URL and back navigation. - const hasIntermediateRoutesOfDifferentType = routesBetween.some((routeBetween) => routeBetween.name !== route.name); - - if (hasIntermediateRenderedRoutes || hasIntermediateRoutesOfDifferentType) { + if (hasIntermediateRenderedRoutes) { return route; } diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 862dd9fdf792..2844878af164 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -240,8 +240,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N // We want to clean saved scroll offsets for screens that aren't anymore in the state. cleanStaleScrollOffsets(state); cleanPreservedNavigatorStates(state); - - console.log('Navigation state changed', state); }; const onReadyWithSentry = useCallback(() => { From fa56030f802051dc2ce33658505a2956cb0d60a4 Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Wed, 11 Mar 2026 14:52:11 +0100 Subject: [PATCH 27/27] working on performance gain --- .../createSplitNavigator/useNavigateOnParamsChange.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts index a301cf4826b4..276b5bf146aa 100644 --- a/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/useNavigateOnParamsChange.ts @@ -1,4 +1,4 @@ -import isEqual from 'lodash/isEqual'; +import {deepEqual} from 'fast-equals'; import {useEffect, useRef} from 'react'; import type {CustomEffectsHookProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -38,7 +38,7 @@ function useNavigateOnParamsChange({navigation, parentRoute, state}: CustomEffec // Already showing the correct screen with the same params — nothing to do. const lastRoute = state.routes.at(-1); - if (lastRoute?.name === screenToNavigate && isEqual(lastRoute?.params, screenParams)) { + if (lastRoute?.name === screenToNavigate && deepEqual(lastRoute?.params, screenParams)) { return; }