Skip to content

Dynamic resolution of Tailwind (Nativewind) tva() utility causes app crash for MFE apps in Release / Production builds #1289

@sahajarora1286

Description

@sahajarora1286

Describe the bug

Hello, I have a React Native Super App configured with Re.pack and Webpack.
It contains a host app and some mini-apps (MFEs).
It utilizes Nativewind / Tailwind and a UI library called Gluestack UI. Gluestack UI components use the tva() utility from tailwind to generate the resulting className strings based on component props (variants, primary, secondary, etc.).

The entire setup works perfectly fine with all Nativewind styling working as expected in dev builds of host and MFE apps.
However, things change for release builds.
The host app and its components render just fine in the release build, but when the host app loads an MFE app screen (MFE release build), the MFE app crashes because apparently the tva() utility's dynamic generation of className based on props returns undefined.

For e.g, this is a Gluestack UI component (ButtonText) used in both host and MFE app app1:

const ButtonText = React.forwardRef<
  React.ComponentRef<typeof UIButton.Text>,
  IButtonTextProps
>(function ButtonText({ className, variant, size, action, ...props }, ref) {
  const {
    variant: parentVariant,
    size: parentSize,
    action: parentAction,
  } = useStyleContext(SCOPE);

  return (
    <UIButton.Text
      ref={ref}
      {...props}
      className={buttonTextStyle({
        parentVariants: {
          variant: parentVariant,
          size: parentSize,
          action: parentAction,
        },
        variant,
        size,
        action,
        class: className,
      })}
    />
  );
});

const buttonTextStyle = tva({
  base: 'text-typography-0 font-semibold web:select-none',
  parentVariants: {
    action: {
      primary:
        'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
      secondary:
        'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
      positive:
        'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
      negative:
        'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
    },
    variant: {
      link: 'data-[hover=true]:underline data-[active=true]:underline',
      outline: '',
      solid:
        'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
    },
    size: {
      xs: 'text-xs',
      sm: 'text-sm',
      md: 'text-base',
      lg: 'text-lg',
      xl: 'text-xl',
    },
  },
  parentCompoundVariants: [
    {
      variant: 'solid',
      action: 'primary',
      class:
        'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
    },
    {
      variant: 'solid',
      action: 'secondary',
      class:
        'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
    },
    {
      variant: 'solid',
      action: 'positive',
      class:
        'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
    },
    {
      variant: 'solid',
      action: 'negative',
      class:
        'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
    },
    {
      variant: 'outline',
      action: 'primary',
      class:
        'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
    },
    {
      variant: 'outline',
      action: 'secondary',
      class:
        'text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700',
    },
    {
      variant: 'outline',
      action: 'positive',
      class:
        'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
    },
    {
      variant: 'outline',
      action: 'negative',
      class:
        'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
    },
  ],
});

The crash causing code in this specific component is the className assignment to UIButton.Text via buttonTextStyle(...). buttonTextStyle(...) uses tva(...) to generate the resulting className string based on dynamic props received, e.g variant, action, size.

On further debugging I found that the real culprit seems to be the compoundVariants in the tva input object. When ButtonText is given a variant (e.g 'solid') and an action (e.g 'primary'), and if there is a compoundVariant in the tva input for 'solid' and 'primary', that's when the crash occurs. Seems like tva is unable to generate the className string for compoundVariants.

So I believe the real culprit here is tree-shaking that takes place when building the production version of the MFE app.

Here's the output and optimization configuration that gets applied to the MFE prod build in webpack:

 /**
     * Configures output.
     * It's recommended to leave it as it is unless you know what you're doing.
     * By default Webpack will emit files into the directory specified under `path`. In order for the
     * React Native app use them when bundling the `.ipa`/`.apk`, they need to be copied over with
     * `Repack.OutputPlugin`, which is configured by default inside `Repack.RepackPlugin`.
     */
    output: {
      clean: true,
      hashFunction: 'xxhash64',
      filename: 'index.bundle',
      chunkFilename: '[name].chunk.bundle',
    },
    /**
     * Configures optimization of the built bundle.
     */
    optimization: {
      /** Enables minification based on values passed from React Native CLI or from fallback. */
      minimize,
      /** Configure minimizer to process the bundle. */
      minimizer: [
        new TerserPlugin({
          test: /\.(js)?bundle(\?.*)?$/i,
          /**
           * Prevents emitting text file with comments, licenses etc.
           * If you want to gather in-file licenses, feel free to remove this line or configure it
           * differently.
           */
          extractComments: false,
          terserOptions: {
            format: {
              comments: false,
            },
          },
        }),
      ],
      chunkIds: 'named',
    },

The MFE prod build generates several build files, most important of them being app1.container.bundle, which is what the host app loads at runtime via ScriptManager.
On inspecting the contents of app1.container.bundle, I realized that it doesn't contain any tailwind classnames like text-primary-600 or typography-0. I also realized that among the built files are some other source files that are chunked, one of them being the Screen that imports and renders the ButtonText component. In this chunk file, I could find references to tailwind classes.

I don't understand how module federation works in the sense that host only loads app1.container.bundle, a file which does not have access to tailwind classes (due to which the tva() function call fails and leads to a crash).
How do I make sure that the nativewind classes are generated for the MFE code in production builds and are available to the MFE bundle when host dynamically loads the MFE ? This should ensure that dynamic className generated via tva() no longer crashes.

Note: The app and MFE builds work just fine if they are built in production mode but with optimization.minimize = false in webpack config. So things really go wrong when minimizing the MFE build.

The crash log doesn't indicate much other than a cryptic error :

E/ReactNativeJS: TypeError: undefined is not a function
    
    This error is located at:
        in Unknown
        in Unknown
        in Unknown
        in Suspense
        in ErrorBoundary
        in RemoteComponent
        in RemoteAppEntryRouteHandler
        in RCTView
        in Unknown
        in Unknown
        in RemoteAppEntryScreen
        in StaticContainer
        in EnsureSingleNavigator
        in SceneView
        in RCTView
        in Unknown
        in RCTView
        in Unknown
        in Unknown
        in Unknown
        in Background
        in Screen
        in RNSScreen
        in Unknown
        in Suspender
        in Suspense
        in Freeze
        in DelayedFreeze
        in InnerScreen
        in Unknown
        in MaybeScreen
        in RNSScreenContainer
        in ScreenContainer
        in MaybeScreenContainer
        in RCTView
        in Unknown
        in SafeAreaProviderCompat
        in BottomTabView
        in PreventRemoveProvider
        in NavigationContent
        in Unknown
        in BottomTabNavigator
        in RCTView
        in Unknown
        in RCTView
        in Unknown
        in Unknown
        in AnimatedComponent(View)
        in Unknown
        in RCTView
        in Unknown
        in Unknown
        in AnimatedComponent(View)
        in Unknown
        in Wrap
        in AnimatedComponent(Wrap)
        in Unknown
        in GestureDetector
        in RNGestureHandlerRootView
        in GestureHandlerRootView
        in Drawer
        in ThemeProvider
        in EnsureSingleNavigator
        in BaseNavigationContainer
        in NavigationContainerInner
        in RCTView
        in Unknown
        in Unknown
        in RootNavigationMobile
        in AppNavigationContainer
        in PersistGate
        in Provider
        in ErrorBoundary
        in SafeAreaEnv
        in RNCSafeAreaProvider
        in SafeAreaProvider
        in SafeAreaProviderShim
        in ToastProvider
        in PortalProvider
        in RCTView
        in Unknown
        in Unknown
        in GluestackUIProvider
        in App
        in RCTView
        in Unknown
        in Unknown
        in AppContainer, js engine: hermes
Image

Any help with this would be super appreciated.

System Info

System:
  OS: macOS 15.5
  CPU: (10) arm64 Apple M1 Max
  Memory: 82.81 MB / 64.00 GB
  Shell:
    version: 3.2.57
    path: /bin/bash
Binaries:
  Node:
    version: 18.20.0
    path: ~/.nvm/versions/node/v18.20.0/bin/node
  Yarn:
    version: 1.19.0
    path: /opt/homebrew/bin/yarn
  npm:
    version: 10.5.0
    path: ~/.nvm/versions/node/v18.20.0/bin/npm
  Watchman: Not Found
Managers:
  CocoaPods:
    version: 1.15.2
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK: Not Found
  Android SDK: Not Found
IDEs:
  Android Studio: 2021.3 AI-213.7172.25.2113.9123335
  Xcode:
    version: /undefined
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.5
    path: /usr/bin/javac
  Ruby:
    version: 2.6.10
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react: Not Found
  react-native:
    installed: 0.74.6
    wanted: "0.74"
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: false
iOS:
  hermesEnabled: true
  newArchEnabled: false

Re.Pack Version

5.0.0-rc.12

Reproduction

Tricky to create a reproduction URL

Steps to reproduce

Tricky to create a reproduction URL

Metadata

Metadata

Assignees

No one assigned

    Labels

    status:newNew issue, not reviewed by the team yet.type:bugA bug report.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions