repack icon indicating copy to clipboard operation
repack copied to clipboard

how to handle unavailability of micro App in the host application

Open vlak-koder opened this issue 1 year ago • 5 comments
trafficstars

Description

Currently there's no way to reload the mini app if fetching fails due to network failure or any other type of failure

My Mini App

const TvNav = React.lazy(() =>
  Federated.importModule('Tv', './Tv'),
);

const Tv = ({ route }: any) => {

  return (
    <ErrorBoundary name="Tv">
      <React.Suspense fallback={<FallbackComponent />}>
        <TvNav params={route?.params} />
      </React.Suspense>
    </ErrorBoundary>
  );
};

export default Tv;

My Error Boundary

import { globalRed100 } from '@/style-dictionary-dist/momoStyle';
import { gpsh } from '@/utils/parseTokenStyle';
import { Box, Button, Icon, SafeAreaContainer, Text } from '@atom';
import { ScriptManager } from '@callstack/repack/client';
import { BackHeadingX } from '@molecule';
import React from 'react';
import { StyleSheet } from 'react-native';

type Props = {
  children: React.ReactNode;
  name: string;
};

type State = {
  hasError: boolean;
};

class ErrorBoundary extends React.Component<Props, State> {
  name: string;

  constructor(props: Props) {
    super(props);
    this.name = props.name;
    this.state = {hasError: false};
  }

  static getDerivedStateFromError() {
    return {hasError: true};
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <SafeAreaContainer>
          <BackHeadingX title="" noCancel />
          <Box style={styles.container}>
            <Icon size={96} color={globalRed100} name="WarningIcon" />
            <Text
              color={'red100'}
              variant={'bold18'}>{`Failed to load ${this.name}`}</Text>
            <Text color={'red100'} variant={'medium16'}>
              try again later
            </Text>
            <Button
              label="go back"
              // variant='secondary'
              bStyle={{
                marginTop: gpsh(10),
              }}
              onPress={async () => {
                try {
                  const ass = await ScriptManager.shared.invalidateScripts([
                    'Startimes',
                  ]);
                  console.log('ass', ass)
                } catch (err) {
                  console.log('err', err);
                }
              }}
            />
          </Box>
        </SafeAreaContainer>
      );
    }

    return this.props.children;
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    marginBottom: gpsh(150),
  },
});

export default ErrorBoundary;

In my error boundary, There's a button that calls ScriptManager.shared.invalidateScripts(['Startimes',])

onPress = async () => {
  try {
    const scriptId = await ScriptManager.shared.invalidateScripts([
      "Startimes",
    ]);
    console.log("scriptId", scriptId);
  } catch (error) {}
};

But it does not do anything.... Have tried unmounting and remounting, it's the same thing. Currently the only way to reload the micro app is to reopen the application and it gets invalidated but if I enable caching, and I reopen the app, I'd be stuck on the Error Boundary screen. So this is the only blocker for enabling caching for me.

There's also a closed issue on this - https://github.com/callstack/repack/issues/526

Also when I logged the ScriptId, I got values like node_modules_babel_runtime_helpers_asyncToGenerator_js-node_modules_babel_runtime_helpers_sli-1b78c7 vendors-node_modules_react-native_Libraries_TurboModule_TurboModuleRegistry_js also I get the scriptId for the mini app - "Startimes"

Suggested solution

No response

Additional context

No response

vlak-koder avatar Aug 27 '24 08:08 vlak-koder

Hi, this is the final step in enabling caching with Repack. If this issue persists, the user will remain stuck on the error screen as it also gets cached too

vlack-coder avatar Sep 01 '24 16:09 vlack-coder

Hey @vlack-coder,

this is a very valid case, I currently have no capacity to handle this but we will look into this in the future for sure. Do you care do some tinkering on your own? I'm happy to help and answer some questions along the way to unblock you if needed

jbroma avatar Sep 09 '24 09:09 jbroma

@jbroma thanks for your response. I'd also like to contribute to this discourse and more issues but can you recommend resources that'd give me more insights on RePack and Webpack in relation to module federation and react native, Thanks.

vlack-coder avatar Sep 09 '24 09:09 vlack-coder

For Re.Pack, we got our docs at re-pack.dev, but they might be slightly outdated in some places.

For the Module Federation stuff, there is the official docs but thats mostly for MF 2 - but it will be the only supported version from V5 forward anyways. There are also multiple articles from Zack Jackson (the creator of MF) available here.

All in all, it's always down to reading the codebase itself I'm afraid 😅

jbroma avatar Sep 09 '24 10:09 jbroma

This issue has been marked as stale because it has been inactive for 30 days. Please update this issue or it will be automatically closed in 14 days.

github-actions[bot] avatar Oct 10 '24 00:10 github-actions[bot]

This issue has been marked as stale because it has been inactive for 30 days. Please update this issue or it will be automatically closed in 14 days.

github-actions[bot] avatar Nov 11 '24 00:11 github-actions[bot]

This issue has been automatically closed because it has been inactive for more than 14 days. Please reopen if you want to add more context.

github-actions[bot] avatar Nov 25 '24 00:11 github-actions[bot]

hey @vlack-coder @vlak-koder have you solved the problem of reloading the module? i am in the same situation and this method invalidateScripts does not do anything and i have to close and reopen the application in order for the module to reload?

@jbroma Is there any way to do this without restarting the app?

krzysztofzaluski avatar Feb 17 '25 18:02 krzysztofzaluski

Hi @krzysztofzaluski, No, I was not able to resolve this. tbvh, It's a not-so-great UX for users. The company I worked for used it regardless but for me it's kinda a big deal production-wise

vlack-coder avatar Feb 19 '25 18:02 vlack-coder

@vlack-coder @krzysztofzaluski you can try approach like below for adding support for retry. this should allow u to retry to load miniapp when it fails to load bundles initially.

import { loadRemote } from '@module-federation/enhanced/runtime';
import React, { ComponentType, useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, ActivityIndicator, StyleSheet } from 'react-native';

export interface MicroAppContext {
  userData: unknown;
}

export const MicroAppComponent = ({
  module,
  context,
}: {
  module: ComponentType<{ context: MicroAppContext }>;
  context: MicroAppContext;
}) => {
  if (!module) {
    return null;
  }
  const Component = module;
  return <Component context={context} />;
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontSize: 24,
    textAlign: 'center',
  },
});

export const withRetry = (moduleName: string) => () => {
  return (props: { context: MicroAppContext }) => {
    const [module, setModule] = useState<ComponentType<{ context: MicroAppContext }> | null>(null);
    const [error, setError] = useState(null);
    const [loading, setLoading] = useState(true);
    const [retryCount, setRetryCount] = useState(0);

    const loadModule = () => {
      setLoading(true);
      loadRemote<{ default: ComponentType<{ context: MicroAppContext }> }>(moduleName)
        .then((m) => {
          console.log('Loaded module:', m);
          if (m) {
            setModule(() => m.default);
            setLoading(false);
            setError(null);
          }
        })
        .catch((err) => {
          console.error(`Error loading ${moduleName}:`, err);
          setError(err);
          setLoading(false);
        });
    };

    useEffect(() => {
      loadModule();
    }, [retryCount]);

    const onRetry = () => {
      setRetryCount(retryCount + 1);
    };

    if (loading) {
      return (
        <View style={{ justifyContent: 'center', alignItems: 'center', flex: 1 }}>
          <ActivityIndicator color={'black'} size="large" />
        </View>
      );
    }

    if (error || !module) {
      return (
        <View style={styles.container}>
          <Text style={styles.text}>Error loading {moduleName}.</Text>
          <TouchableOpacity onPress={onRetry}>
            <Text>Retry</Text>
          </TouchableOpacity>
        </View>
      );
    }

    return <MicroAppComponent {...props} module={module} />;
  };
};

mlakmal avatar May 21 '25 16:05 mlakmal

Hi @mlakmal , thanks for your solution. Though I've also implemented on my end — It's somewhat in the example in the repo. What is your versioning and caching strategy?

vlack-coder avatar May 23 '25 07:05 vlack-coder

@jbroma thank you for adding MF2 and I like that we can register the remote anywhere in the app. Just one thing as regards RSPack.

  • I am not too sure but the app startup time seems longer than with metro or even webpack - 14 secs on a slow Android phone though (and it's not even a cold start)

  • The JS Bundle for RSPack is almost double the size of either of metro or webpack which is not favourable for codepush update

  • Lastly, I was able to create a bundle for webpack and run the app in debug mode but when I create release app it crashes and gives this error

TypeError: undefined is not a function

This error is located at:
    at AppContainer (address at index.android.bundle:1:2002929), js engine: hermes

This is the command I use to bundle my code

  "bundleAndroid:rspack": "find ./CodePush/Android -mindepth 1 -delete && react-native bundle --platform android  --entry-file index.js  --bundle-output ./CodePush/Android/index.android.bundle --assets-dest ./CodePush/Android  --dev false",

Is there any flag I can add to optimize the bundle

vlack-coder avatar May 23 '25 09:05 vlack-coder

Hi @mlakmal , thanks for your solution. Though I've also implemented on my end — It's somewhat in the example in the repo. What is your versioning and caching strategy?

@vlack-coder Bundles are located in versioned folders ex: domain/miniapps/auth/v1/* or domain/miniapps/auth/v2/*

We just use script manager caching and it works without any custom solution...

mlakmal avatar May 23 '25 10:05 mlakmal