Building Micro-Frontends in React Native: Module Federation with Re.Pack, Dynamic Chunk Splitting, and Super-App Architecture
Executive Summary (TL;DR): Scaling large mobile applications across multiple autonomous feature teams requires moving beyond monolithic codebases without fragmenting the user experience. This guide provides an end-to-end implementation for building a React Native super-app using Callstack's Re.Pack and Module Federation. You will learn how to configure a host shell, build independently deployable mini-app remotes, dynamically fetch chunks on demand, and prevent common runtime pitfalls.
Introduction: The Problem with Monolithic React Native Apps
Monolithic React Native architectures force multiple domain teams to share a single repository or bundle, causing continuous release bottlenecks, CI/CD pipeline congestion, and tight coupling between unrelated features. A micro-frontend (or super-app) architecture resolves this by isolating domain logic into autonomous mini-apps that can be developed, tested, and released independently over the wire. Leveraging Re.Pack on GitHub replaces the traditional Metro bundler with Webpack/Rspack, unlocking native Module Federation and dynamic chunk resolution for mobile platforms.
In standard React Native setups, the Metro bundler creates a single static JavaScript bundle (index.android.bundle or main.jsbundle) compiled ahead of time. When team sizes scale from ten engineers to hundreds across domains (e.g., Auth, Payments, Catalog), shipping updates requires coordinating release branches, risking regressions across unrelated features, and running massive native compilation steps for minor UI updates.
Micro-frontends solve this operational constraint by breaking the application into two core primitives:
- The Host (Shell Container): A native binary containing the bare minimum configuration: navigation, authentication context, native modules, and runtime orchestration.
- The Remotes (Mini-Apps): Pure JavaScript/TypeScript packages that expose isolated feature screens or sub-trees (e.g., checkout flows or account settings) built and deployed to a remote CDN.
Step 1: Tooling and Workspace Setup
Setting up micro-frontends requires replacing Metro with Re.Pack in both the host container and remote mini-apps. Each project requires @callstack/repack alongside Webpack or Rspack dependencies configured to emit federated bundles. This setup establishes independent repositories or monorepo packages that communicate through standardized Module Federation contracts.
1. Initialize the Workspace Structure
A common structure consists of either an npm/pnpm/yarn monorepo or separated repositories. For clarity, we will structure this guide around a monorepo workspace containing a host app and a checkout-remote mini-app:
superapp-root/
├── package.json
├── apps/
│ ├── host/ # Host container (owns native iOS/Android directories)
│ │ ├── src/
│ │ ├── webpack.config.mjs
│ │ └── package.json
│ └── checkout-remote/ # Pure JS mini-app (no native directories required)
│ ├── src/
│ ├── webpack.config.mjs
│ └── package.json2. Install Required Dependencies
In your apps/host directory, install Re.Pack and its peer dependencies:
# Inside apps/host
npm install --save-dev @callstack/repack webpack terser-webpack-plugin
npm install @callstack/repack/clientIn your apps/checkout-remote directory, install the same tooling versions:
# Inside apps/checkout-remote
npm install --save-dev @callstack/repack webpack terser-webpack-plugin3. Update React Native CLI Configuration
In apps/host/react-native.config.js, delegate bundling commands from Metro to Re.Pack:
module.exports = {
commands: require('@callstack/repack/commands'),
};Update the scripts in both package.json files to run Re.Pack's dev server and bundle commands:
{
"scripts": {
"start": "react-native webpack-start",
"bundle:ios": "react-native webpack-bundle --platform ios",
"bundle:android": "react-native webpack-bundle --platform android"
}
}Step 2: Configuring Module Federation with Re.Pack
Configuring Module Federation involves defining the host container as a consumer and remote mini-apps as module exporters inside their respective bundler configurations. Re.Pack exposes a dedicated Repack.plugins.ModuleFederationPlugin that coordinates chunk splitting and dependency sharing across apps. Properly declaring shared dependencies like React and React Native as singletons prevents dual-runtime initialization crashes at runtime.
1. Configure the Remote Mini-App (apps/checkout-remote/webpack.config.mjs)
The remote mini-app defines an exposed module (./CheckoutScreen) that the host can request dynamically:
import * as Repack from '@callstack/repack';
export default (env) => {
const { mode = 'development', platform = 'ios' } = env;
return {
mode,
context: Repack.getContext(),
entry: {}, // Mini-app does not need a standard local entry point when federated
resolve: {
...Repack.getResolveOptions(platform),
},
output: {
clean: true,
path: Repack.getOutputPath(mode, platform),
filename: '[name].bundle',
chunkFilename: '[name].chunk.bundle',
},
module: {
rules: [
...Repack.getModuleRules({ platform }),
],
},
plugins: [
new Repack.RepackPlugin({
platform,
}),
new Repack.plugins.ModuleFederationPlugin({
name: 'checkout',
exposes: {
'./CheckoutScreen': './src/CheckoutScreen.tsx',
},
shared: {
react: {
singleton: true,
eager: false,
requiredVersion: '18.3.1',
},
'react-native': {
singleton: true,
eager: false,
requiredVersion: '0.76.0',
},
},
}),
],
};
};2. Configure the Host Shell Container (apps/host/webpack.config.mjs)
The host must configure Module Federation with eager-loaded shared dependencies to ensure core native bindings are instantly available:
import * as Repack from '@callstack/repack';
export default (env) => {
const { mode = 'development', platform = 'ios' } = env;
return {
mode,
context: Repack.getContext(),
entry: './index.js',
resolve: {
...Repack.getResolveOptions(platform),
},
output: {
clean: true,
path: Repack.getOutputPath(mode, platform),
filename: 'index.bundle',
chunkFilename: '[name].chunk.bundle',
},
module: {
rules: [
...Repack.getModuleRules({ platform }),
],
},
plugins: [
new Repack.RepackPlugin({
platform,
}),
new Repack.plugins.ModuleFederationPlugin({
name: 'host',
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '18.3.1',
},
'react-native': {
singleton: true,
eager: true,
requiredVersion: '0.76.0',
},
},
}),
],
};
};Step 3: Implementing Dynamic Chunk Splitting in the Host Application
Dynamic chunk splitting allows the host container to fetch and mount remote mini-apps on demand rather than bundling them into the initial startup payload. Using Re.Pack's dynamic script loader and URL resolvers, the host resolves remote bundle endpoints from a remote server or CDN at runtime. Wrapping remote components in standard React Suspense and ErrorBoundary components ensures smooth loading states and resilient fault isolation.
1. Initialize the ScriptManager and Remote URL Resolver
In the host's entry point (apps/host/index.js), configure Re.Pack's ScriptManager to dynamically locate remote bundles according to platform and environment:
// apps/host/src/bootstrap.ts
import { ScriptManager, Script, Federated } from '@callstack/repack/client';
import { Platform } from 'react-native';
const REMOTE_URL_MAP: Record<string, string> = {
checkout: __DEV__
? 'http://localhost:9001'
: 'https://cdn.example.com/mini-apps/checkout',
};
ScriptManager.shared.addResolver(async (scriptId, caller) => {
const resolveURL = Federated.createRemoteUrlResolver({
containers: REMOTE_URL_MAP,
});
const url = resolveURL(scriptId, caller);
if (url) {
return {
url,
query: {
platform: Platform.OS,
},
};
}
return undefined;
});2. Loading the Remote Component with React.lazy
Create a wrapper component in the host that lazily loads the remote CheckoutScreen with fallback handling:
// apps/host/src/screens/CheckoutHostScreen.tsx
import React, { Suspense } from 'react';
import { View, Text, ActivityIndicator, StyleSheet, TouchableOpacity } from 'react-native';
import { Federated } from '@callstack/repack/client';
// Dynamically import the federated container and component
const RemoteCheckoutScreen = React.lazy(() =>
Federated.importModule('checkout', './CheckoutScreen')
);
interface ErrorBoundaryProps {
children: React.ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
}
class RemoteErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error) {
console.error('Failed to mount remote checkout module:', error);
}
render() {
if (this.state.hasError) {
return (
<View style={styles.centerContainer}>
<Text style={styles.errorText}>Unable to load the Checkout screen.</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={() => this.setState({ hasError: false })}
>
<Text style={styles.buttonText}>Retry</Text>
</TouchableOpacity>
</View>
);
}
return this.props.children;
}
}
export const CheckoutHostScreen: React.FC = () => {
return (
<RemoteErrorBoundary>
<Suspense
fallback={
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color="#0066CC" />
<Text style={styles.loadingText}>Fetching Checkout Module...</Text>
</View>
}
>
<RemoteCheckoutScreen />
</Suspense>
</RemoteErrorBoundary>
);
};
const styles = StyleSheet.create({
centerContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 16,
},
loadingText: {
marginTop: 12,
fontSize: 14,
color: '#666',
},
errorText: {
fontSize: 16,
color: '#D32F2F',
marginBottom: 16,
},
retryButton: {
backgroundColor: '#0066CC',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
},
buttonText: {
color: '#FFFFFF',
fontWeight: '600',
},
});Step 4: Common Pitfalls and Best Practices for Super-Apps
Super-app architectures introduce distributed operational challenges such as native bridge mismatches, version drift, and offline network volatility. To prevent runtime crashes, all shared native dependencies must be embedded in the host shell binary and pinned across remote packages. Implementing robust asset resolution, offline chunk caching, and contract validation ensures remote mini-apps degrade gracefully when network requests fail.
| Architectural Area | Common Anti-Pattern | Recommended Production Practice |
|---|---|---|
| Native Module Dependencies | Mini-app adds native packages (e.g., Camera, Bluetooth) without updating the Host binary. | Host maintains a centralized library of allowed native modules; mini-apps compile purely as JavaScript/TypeScript. |
| Shared Singletons | Omitting singleton: true in ModuleFederationPlugin configuration. |
Enforce singleton: true on react, react-native, and navigation state providers to prevent dual-instance memory leaks. |
| Network & Offline Caching | Relying exclusively on HTTP network fetches on every screen navigation. | Configure ScriptManager disk caching or prefetch critical chunks during idle app runtime. |
| Versioning & Releases | Hardcoding absolute bundle URLs directly inside components. | Query a dynamic manifests service (/api/v1/mini-app-manifest) to resolve versioned CDN bundle paths by target app version. |
1. The Native Module Constraint
Remote mini-apps are distributed over the wire as pure JavaScript bundles. They cannot introduce new native code (Objective-C/Swift, Java/Kotlin) at runtime without an App Store or Google Play Store native binary update. The host container must serve as the superset provider for all native capabilities needed by downstream mini-apps.
2. Strict Singleton Management
If two distinct copies of React or React Native are evaluated within the JavaScript runtime, the app will terminate abruptly (e.g., Invariant Violation: ReactCurrentDispatcher.current is null or multiple bridge registrations). Always set:
shared: {
react: { singleton: true, eager: true },
'react-native': { singleton: true, eager: true },
}3. Implementing Offline Script Caching
Mobile devices encounter frequent packet loss and offline states. Re.Pack's ScriptManager provides persistent storage support via local device caches:
import { ScriptManager, ScriptStorage } from '@callstack/repack/client';
// Configure caching strategy
ScriptManager.shared.setStorage(ScriptStorage);When caching is enabled, Re.Pack stores fetched remote bundles in application storage, checking standard cache-control headers or custom version hashes before attempting network revalidation.
Frequently Asked Questions
How do I configure remote URL resolution for different environments (Dev, Staging, Prod)?
Use a remote manifest API or environment configuration dictionary passed to Federated.createRemoteUrlResolver. In development, configure your resolver to map container names directly to local Webpack dev servers (http://localhost:9001, http://localhost:9002). In staging and production, point the resolver to your CDN endpoints partitioned by semantic version or git commit hash (e.g., https://cdn.example.com/checkout/v2.4.1/ios/).
Can mini-apps introduce their own native modules independently?
No. Because iOS and Android require native binary compilation, code signing, and packaging before execution, any library requiring native code (react-native-reanimated, react-native-camera, etc.) must already be linked and compiled inside the host application binary. If a mini-app requires a new native module, the host must install the dependency, release an updated binary through the App Store and Google Play, and only then can the mini-app begin calling the bridge methods.
How does offline caching work when loading remote Re.Pack chunks?
Re.Pack's ScriptManager integrates with persistent file-system storage through custom storage providers or built-in caching flags. When resolving a script via addResolver, you can flag the asset for caching:
ScriptManager.shared.addResolver(async (scriptId, caller) => {
return {
url: `https://cdn.example.com/${caller}/${scriptId}.bundle`,
cache: true,
};
});When a network connection is unavailable, ScriptManager inspects the local application cache directory. If a valid, previously downloaded bundle exists for that chunk, it boots directly from disk without blocking the UI.
About the Author
Furkan Çetinkaya is a Mobile-focused Software Developer specializing in React Native, native bridge integrations (Kotlin & Swift), and supporting backend services. Experienced in maintaining high-impact mobile applications and developer SDKs.