← Volver al listado de tecnologías

React Native con Re.Pack: Module Federation para Apps Móviles

Por: Artiko
react-nativere.packwebpackmodule-federationmicro-frontendscode-splitting

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

  1. Module Federation: Compartir código entre aplicaciones en tiempo de ejecución
  2. Code Splitting Dinámico: Cargar módulos bajo demanda
  3. Micro-frontends: Arquitectura de aplicaciones modulares
  4. Ecosistema Webpack: Acceso a miles de plugins y loaders
  5. Hot Module Replacement Mejorado: HMR más rápido y confiable
  6. Aliases y Resolución Avanzada: Configuración más flexible de rutas

Casos de Uso Ideales

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.