React Native con Re.Pack: Module Federation para Apps Móviles
React Native con Re.Pack: Module Federation para Apps Móviles
¿Qué es Re.Pack?
Re.Pack es un bundler basado en Webpack para React Native que permite características avanzadas como Module Federation, code splitting dinámico y desarrollo de micro-frontends en aplicaciones móviles. Es una alternativa poderosa a Metro que abre nuevas posibilidades arquitectónicas.
¿Por qué Re.Pack?
Ventajas sobre Metro
- Module Federation: Compartir código entre aplicaciones en tiempo de ejecución
- Code Splitting Dinámico: Cargar módulos bajo demanda
- Micro-frontends: Arquitectura de aplicaciones modulares
- Ecosistema Webpack: Acceso a miles de plugins y loaders
- Hot Module Replacement Mejorado: HMR más rápido y confiable
- Aliases y Resolución Avanzada: Configuración más flexible de rutas
Casos de Uso Ideales
- Super Apps: Aplicaciones con múltiples mini-apps independientes
- White-label Apps: Productos personalizables para diferentes clientes
- Aplicaciones Empresariales: Apps grandes con equipos distribuidos
- Marketplace de Plugins: Apps extensibles con módulos de terceros
Instalación y Configuración
1. Crear Nuevo Proyecto
# Crear proyecto React Native
npx react-native init MyRePackApp --template react-native-template-typescript
cd MyRePackApp
# Instalar Re.Pack y dependencias
npm install --save-dev @callstack/repack webpack terser-webpack-plugin
npm install react-native-url-polyfill
2. Configuración de Re.Pack
Crear webpack.config.mjs en la raíz:
import path from 'path';
import TerserPlugin from 'terser-webpack-plugin';
import { createRequire } from 'module';
import * as Repack from '@callstack/repack';
const require = createRequire(import.meta.url);
export default (env) => {
const {
mode = 'development',
context = __dirname,
entry = './index.js',
platform = process.env.PLATFORM,
minimize = mode === 'production',
devServer = undefined,
bundleFilename = undefined,
sourceMapFilename = undefined,
assetsPath = undefined,
reactNativePath = require.resolve('react-native'),
} = env;
if (!platform) {
throw new Error('Platform not specified');
}
const dirname = Repack.getDirname(import.meta.url);
const root = path.resolve(dirname, '..');
return {
mode,
devtool: false,
context: root,
entry: {
main: entry,
},
resolve: {
...Repack.getResolveOptions(platform),
alias: {
'@': path.resolve(root, 'src'),
'@components': path.resolve(root, 'src/components'),
'@screens': path.resolve(root, 'src/screens'),
'@utils': path.resolve(root, 'src/utils'),
'@services': path.resolve(root, 'src/services'),
},
},
output: {
path: path.join(root, 'build/generated', platform),
filename: 'index.bundle',
chunkFilename: '[name].chunk.bundle',
publicPath: Repack.getPublicPath({ platform, devServer }),
},
optimization: {
minimize,
minimizer: [
new TerserPlugin({
terserOptions: {
keep_fnames: true,
},
}),
],
chunkIds: 'named',
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
include: [
/node_modules(.*[/\\])+react-native/,
/node_modules(.*[/\\])+@react-native/,
/node_modules(.*[/\\])+@react-navigation/,
/node_modules(.*[/\\])+@react-native-community/,
/node_modules(.*[/\\])+expo/,
/node_modules(.*[/\\])+pretty-format/,
/node_modules(.*[/\\])+metro/,
/node_modules(.*[/\\])+abort-controller/,
/node_modules(.*[/\\])+@callstack[/\\]repack/,
],
use: 'babel-loader',
},
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: devServer && devServer.hmr ? ['react-refresh/babel'] : [],
},
},
},
{
test: Repack.getAssetExtensionsRegExp(Repack.ASSET_EXTENSIONS),
use: {
loader: '@callstack/repack/assets-loader',
options: {
platform,
devServerEnabled: Boolean(devServer),
scalableAssetExtensions: Repack.SCALABLE_ASSETS,
},
},
},
],
},
plugins: [
new Repack.RepackPlugin({
context,
mode,
platform,
devServer,
output: {
bundleFilename,
sourceMapFilename,
assetsPath,
},
}),
],
};
};
3. Actualizar Scripts
En package.json:
{
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native webpack-start --port 8081",
"bundle:ios": "react-native webpack-bundle --platform ios --entry-file index.js --dev false",
"bundle:android": "react-native webpack-bundle --platform android --entry-file index.js --dev false"
}
}
4. Configurar React Native
En react-native.config.js:
module.exports = {
commands: require('@callstack/repack/commands'),
};
Code Splitting Básico
1. Configurar Chunks Asíncronos
// src/screens/LazyScreen.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
const LazyScreen: React.FC = () => {
return (
<View style={styles.container}>
<Text style={styles.title}>Pantalla Cargada Dinámicamente!</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
});
export default LazyScreen;
2. Implementar Carga Dinámica
// src/components/AsyncBoundary.tsx
import React, { Suspense, lazy, useState } from 'react';
import {
View,
Button,
ActivityIndicator,
Text,
StyleSheet,
} from 'react-native';
const AsyncBoundary: React.FC = () => {
const [LazyComponent, setLazyComponent] = useState<React.ComponentType | null>(null);
const loadComponent = async () => {
const module = await import('../screens/LazyScreen');
setLazyComponent(() => module.default);
};
return (
<View style={styles.container}>
<Button title="Cargar Componente" onPress={loadComponent} />
{LazyComponent && (
<Suspense fallback={<ActivityIndicator size="large" />}>
<LazyComponent />
</Suspense>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
});
export default AsyncBoundary;
Module Federation
1. Configuración del Host App
// webpack.config.host.mjs
import { ModuleFederationPlugin } from '@callstack/repack/federated';
export default (env) => {
// ... configuración base ...
return {
// ... otras configuraciones ...
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
miniapp: 'miniapp@http://localhost:8082/remoteEntry.js',
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '18.2.0',
},
'react-native': {
singleton: true,
eager: true,
requiredVersion: '0.72.0',
},
},
}),
// ... otros plugins ...
],
};
};
2. Configuración de Mini App Remota
// webpack.config.miniapp.mjs
import { ModuleFederationPlugin } from '@callstack/repack/federated';
export default (env) => {
return {
// ... configuración base ...
plugins: [
new ModuleFederationPlugin({
name: 'miniapp',
filename: 'remoteEntry.js',
exposes: {
'./MiniApp': './src/MiniApp',
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '18.2.0',
},
'react-native': {
singleton: true,
eager: true,
requiredVersion: '0.72.0',
},
},
}),
],
};
};
3. Usar Módulos Federados
// src/screens/FederatedScreen.tsx
import React, { Suspense } from 'react';
import { View, ActivityIndicator, Text } from 'react-native';
import { Federated } from '@callstack/repack/federated';
const MiniApp = Federated.importModule('miniapp', './MiniApp');
const FederatedScreen: React.FC = () => {
return (
<View style={{ flex: 1 }}>
<Suspense
fallback={
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
<Text>Cargando Mini App...</Text>
</View>
}
>
<MiniApp />
</Suspense>
</View>
);
};
export default FederatedScreen;
Optimizaciones Avanzadas
1. Chunks Optimization
// webpack.config.mjs - optimization section
optimization: {
minimize,
minimizer: [
new TerserPlugin({
terserOptions: {
keep_fnames: true,
},
}),
],
chunkIds: 'named',
splitChunks: {
chunks: 'all',
cacheGroups: {
default: false,
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
priority: 10,
enforce: true,
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
screens: {
test: /[\\/]src[\\/]screens[\\/]/,
name: 'screens',
chunks: 'async',
priority: 15,
},
},
},
},
2. Asset Optimization
// src/utils/assetLoader.ts
import { Platform } from 'react-native';
interface AssetModule {
default: any;
}
export async function loadAsset(assetName: string): Promise<any> {
try {
let module: AssetModule;
if (Platform.OS === 'ios') {
module = await import(`../assets/ios/${assetName}`);
} else {
module = await import(`../assets/android/${assetName}`);
}
return module.default;
} catch (error) {
console.error(`Error loading asset ${assetName}:`, error);
return null;
}
}
// Uso
const MyComponent = () => {
const [logo, setLogo] = useState(null);
useEffect(() => {
loadAsset('logo.png').then(setLogo);
}, []);
return logo ? <Image source={logo} /> : <ActivityIndicator />;
};
Desarrollo con HMR
1. Configurar React Refresh
// babel.config.js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
process.env.NODE_ENV === 'development' && 'react-refresh/babel',
].filter(Boolean),
};
2. Componente con HMR
// src/components/DevComponent.tsx
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
const DevComponent: React.FC = () => {
const [count, setCount] = useState(0);
// Este estado se preservará durante HMR
return (
<View style={styles.container}>
<Text style={styles.text}>
Contador: {count}
</Text>
<Button
title="Incrementar"
onPress={() => setCount(c => c + 1)}
/>
<Text style={styles.info}>
Modifica este texto y verás HMR en acción!
</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
backgroundColor: '#f0f0f0',
borderRadius: 8,
},
text: {
fontSize: 18,
marginBottom: 10,
},
info: {
marginTop: 10,
color: '#666',
fontStyle: 'italic',
},
});
export default DevComponent;
Arquitectura de Micro-frontends
1. Estructura del Proyecto
monorepo/
├── packages/
│ ├── host-app/ # Aplicación principal
│ ├── auth-miniapp/ # Mini-app de autenticación
│ ├── shopping-miniapp/ # Mini-app de compras
│ └── shared/ # Código compartido
├── package.json
├── lerna.json
└── tsconfig.json
2. Host App Principal
// packages/host-app/src/App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { ScriptManager, Federated } from '@callstack/repack/federated';
// Registrar mini-apps remotas
ScriptManager.shared.addResolver(async (scriptId, caller) => {
const resolveURL = Federated.createURLResolver({
containers: {
auth: 'http://localhost:8082/[name].container.js',
shopping: 'http://localhost:8083/[name].container.js',
},
});
return resolveURL(scriptId, caller);
});
const Tab = createBottomTabNavigator();
// Lazy load mini-apps
const AuthMiniApp = React.lazy(() =>
Federated.importModule('auth', './AuthApp')
);
const ShoppingMiniApp = React.lazy(() =>
Federated.importModule('shopping', './ShoppingApp')
);
const App: React.FC = () => {
return (
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Auth">
{() => (
<React.Suspense fallback={<LoadingScreen />}>
<AuthMiniApp />
</React.Suspense>
)}
</Tab.Screen>
<Tab.Screen name="Shopping">
{() => (
<React.Suspense fallback={<LoadingScreen />}>
<ShoppingMiniApp />
</React.Suspense>
)}
</Tab.Screen>
</Tab.Navigator>
</NavigationContainer>
);
};
export default App;
3. Mini-app Ejemplo
// packages/auth-miniapp/src/AuthApp.tsx
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import LoginScreen from './screens/LoginScreen';
import RegisterScreen from './screens/RegisterScreen';
import ProfileScreen from './screens/ProfileScreen';
const Stack = createStackNavigator();
export const AuthApp: React.FC = () => {
return (
<Stack.Navigator>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
};
export default AuthApp;
Debugging y Herramientas
1. Source Maps
// webpack.config.mjs
export default (env) => {
const { mode } = env;
return {
devtool: mode === 'development' ? 'source-map' : false,
// ... resto de configuración
};
};
2. Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer
// webpack.config.mjs
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
plugins: [
process.env.ANALYZE && new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
}),
].filter(Boolean),
3. Debug de Module Federation
// src/utils/federationDebug.ts
import { ScriptManager } from '@callstack/repack/federated';
export function setupFederationDebug() {
if (__DEV__) {
// Log todas las resoluciones de scripts
const originalResolver = ScriptManager.shared.resolve;
ScriptManager.shared.resolve = async (scriptId, caller) => {
console.log(`[Federation] Resolviendo: ${scriptId} desde ${caller}`);
try {
const result = await originalResolver.call(
ScriptManager.shared,
scriptId,
caller
);
console.log(`[Federation] Resuelto: ${scriptId} -> ${result.url}`);
return result;
} catch (error) {
console.error(`[Federation] Error resolviendo ${scriptId}:`, error);
throw error;
}
};
}
}
Mejores Prácticas
1. Estructura de Carpetas
src/
├── components/
│ ├── common/ # Componentes compartidos
│ ├── lazy/ # Componentes con carga diferida
│ └── federated/ # Componentes federados
├── screens/
│ ├── sync/ # Pantallas síncronas
│ └── async/ # Pantallas asíncronas
├── services/
├── utils/
│ ├── federation.ts # Utilidades de federación
│ └── chunks.ts # Gestión de chunks
└── assets/
├── ios/
└── android/
2. Gestión de Errores
// src/components/ErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';
import { View, Text, Button } from 'react-native';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
state: State = {
hasError: false,
error: null,
};
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
console.error('ErrorBoundary caught:', error, errorInfo);
// Enviar a servicio de monitoreo
if (!__DEV__) {
// Sentry, Bugsnag, etc.
}
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Algo salió mal</Text>
<Button
title="Reintentar"
onPress={() => this.setState({ hasError: false, error: null })}
/>
</View>
);
}
return this.props.children;
}
}
// Uso con módulos federados
<ErrorBoundary fallback={<Text>Error cargando módulo</Text>}>
<Suspense fallback={<ActivityIndicator />}>
<FederatedComponent />
</Suspense>
</ErrorBoundary>
3. Performance Monitoring
// src/utils/performance.ts
export class ChunkLoadMonitor {
private static instance: ChunkLoadMonitor;
private loadTimes: Map<string, number> = new Map();
static getInstance(): ChunkLoadMonitor {
if (!this.instance) {
this.instance = new ChunkLoadMonitor();
}
return this.instance;
}
async measureChunkLoad<T>(
chunkName: string,
loader: () => Promise<T>
): Promise<T> {
const startTime = Date.now();
try {
const result = await loader();
const loadTime = Date.now() - startTime;
this.loadTimes.set(chunkName, loadTime);
console.log(`[Chunk] ${chunkName} loaded in ${loadTime}ms`);
return result;
} catch (error) {
console.error(`[Chunk] Failed to load ${chunkName}:`, error);
throw error;
}
}
getMetrics() {
return Array.from(this.loadTimes.entries()).map(([chunk, time]) => ({
chunk,
loadTime: time,
}));
}
}
// Uso
const monitor = ChunkLoadMonitor.getInstance();
const LazyComponent = await monitor.measureChunkLoad(
'LazyScreen',
() => import('../screens/LazyScreen')
);
Conclusión
Re.Pack transforma React Native en una plataforma capaz de soportar arquitecturas complejas como micro-frontends y aplicaciones modulares. Su integración con Webpack abre un mundo de posibilidades para optimización y modularización que no son posibles con Metro.
Recursos Adicionales
En el próximo tutorial, exploraremos casos de uso avanzados como la creación de un marketplace de plugins y la implementación de actualizaciones OTA (Over-The-Air) con Re.Pack.