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};