diff --git a/package-lock.json b/package-lock.json index de12a7d768a94..b1e4c14f6f4f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", "react-native-blob-util": "0.19.4", + "react-native-bundle-splitter": "^3.0.1", "react-native-collapsible": "^1.6.2", "react-native-config": "1.5.0", "react-native-dev-menu": "^4.1.1", @@ -34411,6 +34412,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/react-native-bundle-splitter": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-native-bundle-splitter/-/react-native-bundle-splitter-3.0.1.tgz", + "integrity": "sha512-YvG30oL+3uhPoYisRzMJLHjs5+X7NK8yLHqLKxLIvqZ9wGAvIJ+sJG09iQpLK+UCjEAEVP5he0F0vnzuokHyBw==", + "peerDependencies": { + "react": "*", + "react-native": ">=0.59.1" + } + }, "node_modules/react-native-clean-project": { "version": "4.0.1", "dev": true, diff --git a/package.json b/package.json index 4c1bf98cc9765..c1c2d053be02f 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", "react-native-blob-util": "0.19.4", + "react-native-bundle-splitter": "^3.0.1", "react-native-collapsible": "^1.6.2", "react-native-config": "1.5.0", "react-native-dev-menu": "^4.1.1", diff --git a/patches/@react-navigation+core+6.4.11+002+proper-navigator-unmount-in-strict-mode.patch b/patches/@react-navigation+core+6.4.11+002+proper-navigator-unmount-in-strict-mode.patch new file mode 100644 index 0000000000000..817fd1ea1c7ff --- /dev/null +++ b/patches/@react-navigation+core+6.4.11+002+proper-navigator-unmount-in-strict-mode.patch @@ -0,0 +1,52 @@ +diff --git a/node_modules/@react-navigation/core/lib/module/useNavigationBuilder.js b/node_modules/@react-navigation/core/lib/module/useNavigationBuilder.js +index 6fb49e0..1f7c859 100644 +--- a/node_modules/@react-navigation/core/lib/module/useNavigationBuilder.js ++++ b/node_modules/@react-navigation/core/lib/module/useNavigationBuilder.js +@@ -114,6 +114,16 @@ const getRouteConfigsFromChildren = (children, groupKey, groupOptions) => { + return configs; + }; + ++const useFullyMountedRef = () => { ++ const isFullyMountedRef = React.useRef(false); ++ ++ React.useEffect(() => { ++ isFullyMountedRef.current = true; ++ }, []); ++ ++ return isFullyMountedRef; ++}; ++ + /** + * Hook for building navigators. + * +@@ -122,6 +132,7 @@ const getRouteConfigsFromChildren = (children, groupKey, groupOptions) => { + * @returns An object containing `state`, `navigation`, `descriptors` objects. + */ + export default function useNavigationBuilder(createRouter, options) { ++ const isFullyMountedRef = useFullyMountedRef(); + const navigatorKey = useRegisterNavigator(); + const route = React.useContext(NavigationRouteContext); + const { +@@ -298,10 +309,18 @@ export default function useNavigationBuilder(createRouter, options) { + setState(nextState); + } + return () => { +- // We need to clean up state for this navigator on unmount +- if (getCurrentState() !== undefined && getKey() === navigatorKey) { +- setCurrentState(undefined); +- stateCleanedUp.current = true; ++ const cleanup = () => { ++ // We need to clean up state for this navigator on unmount ++ if (getCurrentState() !== undefined && getKey() === navigatorKey) { ++ setCurrentState(undefined); ++ stateCleanedUp.current = true; ++ } ++ } ++ ++ if (!isFullyMountedRef.current) { ++ cleanup(); ++ } else { ++ setTimeout(cleanup, 0); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/App.tsx b/src/App.tsx index 177cc00c7dee6..33b75ec74338d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,7 +54,7 @@ LogBox.ignoreLogs([ const fill = {flex: 1}; -const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; +const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : React.Fragment; function App({url}: AppProps) { useDefaultDragAndDrop(); diff --git a/src/libs/Navigation/AppNavigator/index.tsx b/src/libs/Navigation/AppNavigator/index.tsx index 05961528ca115..ac70d7167246b 100644 --- a/src/libs/Navigation/AppNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/index.tsx @@ -1,22 +1,37 @@ -import React, {lazy, memo, Suspense} from 'react'; -import lazyRetry from '@src/utils/lazyRetry'; +import React, {lazy, memo, Suspense, useEffect, useState} from 'react'; +import {preload, register} from 'react-native-bundle-splitter'; +import lazyRetry, {retryImport} from '@src/utils/lazyRetry'; -const AuthScreens = lazy(() => lazyRetry(() => import('./AuthScreens'))); const PublicScreens = lazy(() => lazyRetry(() => import('./PublicScreens'))); +const AUTH_SCREENS = 'AuthScreens'; +const AuthScreens = register({ + name: AUTH_SCREENS, + loader: () => retryImport(() => import('./AuthScreens')), +}); + type AppNavigatorProps = { /** If we have an authToken this is true */ authenticated: boolean; }; function AppNavigator({authenticated}: AppNavigatorProps) { - if (authenticated) { + const [canNavigateToProtectedRoutes, setNavigateToProtectedRoutes] = useState(false); + + useEffect(() => { + // Preload Auth Screens in advance to be sure that navigator can be mounted synchronously + // to avoid problems described in https://github.com/Expensify/App/issues/44600 + preload() + .component(AUTH_SCREENS) + .then(() => { + setNavigateToProtectedRoutes(true); + }); + }, []); + + if (authenticated && canNavigateToProtectedRoutes) { // These are the protected screens and only accessible when an authToken is present - return ( - - - - ); + // Navigate to them only when route is preloaded + return ; } return ( diff --git a/src/utils/lazyRetry.ts b/src/utils/lazyRetry.ts index 35c6b444e612b..5bcf8d3d83d05 100644 --- a/src/utils/lazyRetry.ts +++ b/src/utils/lazyRetry.ts @@ -5,6 +5,8 @@ type Import = Promise<{default: T}>; type ComponentImport = () => Import; /** + * Common retry mechanism for importing components. + * * Attempts to lazily import a React component with a retry mechanism on failure. * If the initial import fails the function will refresh the page once and retry the import. * If the import fails again after the refresh, the error is propagated. @@ -12,8 +14,7 @@ type ComponentImport = () => Import; * @param componentImport - A function that returns a promise resolving to a lazily imported React component. * @returns A promise that resolves to the imported component or rejects with an error after a retry attempt. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const lazyRetry = function >(componentImport: ComponentImport): Import { +function retryImport(componentImport: ComponentImport): Import { return new Promise((resolve, reject) => { // Retrieve the retry status from sessionStorage, defaulting to 'false' if not set const hasRefreshed = JSON.parse(sessionStorage.getItem(CONST.SESSION_STORAGE_KEYS.RETRY_LAZY_REFRESHED) ?? 'false') as boolean; @@ -37,6 +38,12 @@ const lazyRetry = function >(componentImport: Compo } }); }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const lazyRetry = function >(componentImport: ComponentImport): Import { + return retryImport(componentImport); }; export default lazyRetry; +export {retryImport};