repack icon indicating copy to clipboard operation
repack copied to clipboard

How to redownload bundle failed and update the screen

Open lequoctrang4 opened this issue 3 months ago • 4 comments

Description

I use react-native 0.77.2 and @callstack/repack 5.1.0 and federation

This is the case: When i load bundle failed for some reason (no internet, link revoke, bundle invalid), i load the fallback-component to replace for handler crash app. And when the internet is back I want the user to be able to click to redownload that bundle.

This is my rs-pack rspack.config.mjs

import pkg from '@btaskee/sdk';
import * as Repack from '@callstack/repack';
import { ReanimatedPlugin } from '@callstack/repack-plugin-reanimated';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const { getLocalIP, getSharedDependencies } = pkg;

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

/**
 * Rspack configuration enhanced with Re.Pack defaults for React Native.
 *
 * Learn about Rspack configuration: https://rspack.dev/config/
 * Learn about Re.Pack configuration: https://re-pack.dev/docs/guides/configuration
 */
export default (env) => {
  const { mode, platform = process.env.PLATFORM } = env;
  const hostIP = getLocalIP();
  return {
    mode,
    context: __dirname,
    entry: './index.js',
    experiments: {
      incremental: mode === 'development',
    },
    resolve: {
      ...Repack.getResolveOptions(),
      alias: {
        '@navigation': path.resolve(__dirname, './src/navigation'),
        '@screens': path.resolve(__dirname, './src/screens'),
        '@app': path.resolve(__dirname, './src/App.tsx'),
        '@hooks': path.resolve(__dirname, './src/hooks'),
        '@images': path.resolve(__dirname, './src/assets/images'),
        '@lottie': path.resolve(__dirname, './src/assets/lottie'),
        '@components': path.resolve(__dirname, './src/components'),
        '@utils': path.resolve(__dirname, './src/utils'),
        '@types': path.resolve(__dirname, './src/types'),
        '@stores': path.resolve(__dirname, './src/stores'),
        '@providers': path.resolve(__dirname, './src/providers'),
      },
    },
    output: {
      uniqueName: 'sas-host',
      path: path.resolve(__dirname, 'dist', platform),
    },
    module: {
      rules: [
        ...Repack.getJsTransformRules(),
        ...Repack.getAssetTransformRules(),
      ],
    },
    plugins: [
      new Repack.RepackPlugin(),
      new ReanimatedPlugin(),
      new Repack.plugins.ModuleFederationPluginV2({
        name: 'host',
        dts: false,
        remotes: {
          error: `error@https://error-447.firebaseapp.com/error.container.js.bundle`,
          account: `account@https://account-247.firebaseapp.com/account.container.js.bundle`,
        },
        shared: getSharedDependencies({ eager: true }),
        runtimePlugins: [path.resolve(__dirname, 'remote-fallback-plugin.ts')],
      }),
    ],
  };
};

remote-fallback-plugin.ts

import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';

const remoteFallbackPlugin = (): FederationRuntimePlugin => {
  return {
    name: 'remote-fallback-plugin',
    async errorLoadRemote(args: any) {
      const FallbackComponent = await import('error/FallbackComponent');

      return () => FallbackComponent;
    },
  };
};

export default remoteFallbackPlugin;

Account

import React from 'react';
import { useRoute } from '@react-navigation/native';

import { ErrorBoundary, LoadingMiniApp } from '@components';

const Account = React.lazy(() => import('account/AccountTab'));

export function TabAccountScreen(): React.JSX.Element {
  const route = useRoute();
  return (
    <ErrorBoundary name="AccountScreen">
      <React.Suspense fallback={<LoadingMiniApp />}>
        <Account {...route.params} />
      </React.Suspense>
    </ErrorBoundary>
  );
}

Because ErrorBoundary can't catch crash app when no have internet, so i use errorLoadRemote to reassign account bundle to error/FallbackComponent (Of course error/FallbackComponent has been preloaded and cached)

I want to implement button retry in the FallbackComponent, When user click, it will redownload bundle and replace bundle error/FallbackComponent to valid bundle.

But now i only can restart App, Is there any other solution?

Suggested solution

No response

Additional context

No response

lequoctrang4 avatar Oct 29 '25 09:10 lequoctrang4

I have the same problem, but I'm working with version "@callstack/repack": "^4.4.1". I currently have 18 microfronts published and in the process of being updated to the latest versions. To solve these very specific cases, especially when users have a very poor internet connection, I had to implement different solutions. One of these was implementing a lazyRetry, which allows me to retry the microfront download, capture the microfront that presents the error, and if it fails despite the retries, since the app closes automatically because the ErrorBoundary doesn't correctly capture the error (something I still need to review), upon reopening, I identify which microfront caused the error and change its URL, specifically the version, even without changing the publication. This is to force the download again, since invalidating the scripts wasn't working correctly despite the attempts. The way I handle the microfront version is simply a configuration in nginx so that, regardless of the version in the URL, the bundles are returned.

eeuruetade avatar Oct 29 '25 22:10 eeuruetade

I have the same problem, but I'm working with version "@callstack/repack": "^4.4.1". I currently have 18 microfronts published and in the process of being updated to the latest versions. To solve these very specific cases, especially when users have a very poor internet connection, I had to implement different solutions. One of these was implementing a lazyRetry, which allows me to retry the microfront download, capture the microfront that presents the error, and if it fails despite the retries, since the app closes automatically because the ErrorBoundary doesn't correctly capture the error (something I still need to review), upon reopening, I identify which microfront caused the error and change its URL, specifically the version, even without changing the publication. This is to force the download again, since invalidating the scripts wasn't working correctly despite the attempts. The way I handle the microfront version is simply a configuration in nginx so that, regardless of the version in the URL, the bundles are returned.

The app still crashes, doesn’t it? Can you share your lazyRetry?

lequoctrang4 avatar Oct 30 '25 04:10 lequoctrang4

It's still failing, but I can prevent it from closing completely. I display a screen indicating a poor connection with a button that resets the navigation to reload everything. If it's just an intermittent issue, retries should fix it. But as I mentioned, I had to force a URL change to reload the microfront.

It's not the best implementation, but it works for me.

export const lazyRetry = (
  componentImport,
  namebundle: string,
  retries = 3,
  delay = 3000,
) => {
  function attempt(remaining) {
    return componentImport()
      .then(resolve => {
        AsyncStorage.removeItem('SHOW_ERROR_BOUNDARY');
        removeUrlMicroFromBundle(namebundle);
        return resolve;
      })
      .catch(error => {
        console.warn('*** Error loading component:', error);

        if (remaining <= 1) {
          AsyncStorage.setItem('SHOW_ERROR_BOUNDARY', 'true').then(() => {
            throw error;
          });
        }
        return new Promise(res => setTimeout(res, delay)).then(() => {
          console.warn('*** Retrying component load:', remaining);
          return attempt(remaining - 1);
        });
      });
  }
  return attempt(retries);
};

// component

const AppUserAccount = React.lazy(() =>
  lazyRetry(
    () =>
      Federated.importModule('user-account', './UserAccount').then(mod =>
        mod && mod.default ? {default: mod.default} : {default: mod},
      ),
    'account-bundle',
  ),
);

const UserAccount = () => {
  return (
    <ErrorBoundary>
      <Suspense fallback={<Loading />}>
        <AppUserAccount />
      </Suspense>
    </ErrorBoundary>
  );
};

eeuruetade avatar Oct 30 '25 13:10 eeuruetade

It's still failing, but I can prevent it from closing completely. I display a screen indicating a poor connection with a button that resets the navigation to reload everything. If it's just an intermittent issue, retries should fix it. But as I mentioned, I had to force a URL change to reload the microfront.

It's not the best implementation, but it works for me.

export const lazyRetry = ( componentImport, namebundle: string, retries = 3, delay = 3000, ) => { function attempt(remaining) { return componentImport() .then(resolve => { AsyncStorage.removeItem('SHOW_ERROR_BOUNDARY'); removeUrlMicroFromBundle(namebundle); return resolve; }) .catch(error => { console.warn('*** Error loading component:', error);

    if (remaining <= 1) {
      AsyncStorage.setItem('SHOW_ERROR_BOUNDARY', 'true').then(() => {
        throw error;
      });
    }
    return new Promise(res => setTimeout(res, delay)).then(() => {
      console.warn('*** Retrying component load:', remaining);
      return attempt(remaining - 1);
    });
  });

} return attempt(retries); };

// component

const AppUserAccount = React.lazy(() => lazyRetry( () => Federated.importModule('user-account', './UserAccount').then(mod => mod && mod.default ? {default: mod.default} : {default: mod}, ), 'account-bundle', ), );

const UserAccount = () => { return ( <ErrorBoundary> <Suspense fallback={<Loading />}> <AppUserAccount /> </Suspense> </ErrorBoundary> ); }; Hi. Can i ask how to prevent crash if deps loaded fail with MF1

hungtrn75 avatar Nov 25 '25 16:11 hungtrn75