Fundamentos de Brownfield
10 ejemplos para decidir cuándo incorporar React Native, reescribir en RN o usar WebView como alternativa - 7 básicos y 3 intermedios.
Busca en todas las páginas de la documentación
10 ejemplos para decidir cuándo incorporar React Native, reescribir en RN o usar WebView como alternativa - 7 básicos y 3 intermedios.
El trabajo brownfield asume que ya distribuyes una app nativa de iOS y/o Android cuyo punto de entrada principal no es React Native. Añades RN como una biblioteca, pantalla o módulo de característica.
Para integración práctica después de estas decisiones, ver Descripción general de expo-brownfield. Para límites de arquitectura dentro de la porción RN, ver Fundamentos de Arquitectura Móvil.
# Spike: proyecto Expo mínimo para validar viabilidad de incorporación
npx create-expo-app@latest RnSpike --template blank-typescript@sdk-57
cd RnSpike
npx expo install expo-brownfieldTooling: Estos ejemplos tienen como objetivo Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 y React 19.2.3. Las APIs de integración brownfield están en alpha - presupuesta tiempo para debugging de compilación nativa.
Greenfield significa que React Native (o Expo) es la raíz de la app - cada pantalla se ramifica desde una entrada JS. Brownfield significa que UIKit/Swift, Jetpack Compose/Kotlin u otro stack nativo posee el shell; RN se incorpora bajo demanda.
Greenfield Brownfield
┌─────────────────────┐ ┌─────────────────────┐
│ RN root (main) │ │ Native root │
│ ├─ Tab A │ │ ├─ Home (native) │
│ ├─ Tab B │ │ ├─ Settings (nat.) │
│ └─ Modal │ │ └─ Checkout (RN) │ ← RN island
└─────────────────────┘ └─────────────────────┘Relacionado: Descripción general de expo-brownfield - empaquetando RN como AAR/XCFramework | ../architecture-design/mobile-architecture-basics/mobile-architecture-basics.md - estructurando la porción RN una vez incorporada
Incorpora RN cuando la característica necesite UI con sentimiento nativo, comportamiento offline/cache, APIs de dispositivo o TypeScript compartido con un equipo web - y la pantalla nativa heredada tomaría trimestres para reconstruir.
| Señal | Incorporar RN |
|---|---|
| Formularios complejos con validación | ✓ |
| Listas con gestos / Reanimated | ✓ |
| Cámara, biometría, BLE vía módulos Expo | ✓ |
| Reutilizar sistema de diseño React existente | ✓ |
| Una página de preguntas frecuentes estática | ✗ (WebView o nativo) |
// La porción RN puede usar Expo Router dentro del contenedor proporcionado por el host
// app/_layout.tsx - la navegación RN es interna al módulo incorporado
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="checkout" />
<Stack.Screen name="order-confirmation" />
</Stack>
);
}expo-brownfield cuando los equipos nativos rechazan Node en CImultipleFrameworks en iOSRelacionado: RNHostView & Incorporación de UI Nativa - RN dentro de layouts de SwiftUI/Compose | ../native-modules/native-modules-basics/native-modules-basics.md - cuando solo JS es insuficiente
Una reescritura reemplaza el shell nativo con una app Expo/RN. Justificada cuando la mayoría de pantallas están cambiando de todos modos, los costos de navegación dual exceden la migración, o necesitas un pipeline OTA para todo el producto.
| Señal | Reescritura |
|---|---|
| >60% del roadmap toca UI compartida con web | ✓ |
| Codebase nativo no mantenible (sin tests, sin dueños) | ✓ |
| Cumplimiento de tienda requiere identidad binaria única | ✓ |
| Un equipo posee una característica en una app por lo demás estable | ✗ (incorporar) |
Lista de verificación de decisión de reescritura (todos "sí" - caso de reescritura fuerte):
□ Nativo y web móvil comparten una librería de componentes
□ Auth, push y deep links necesitan enrutamiento unificado (Expo Router)
□ La capacidad nativa no puede sostener dos stacks UI
□ El liderazgo acepta 2-4 sprints de congelación de migración en flujos afectadosRelacionado: ADR de Adopción Incremental - decisiones strangler fig clasificadas | ../architecture-design/adr-navigation-library-choice/adr-navigation-library-choice.md - Expo Router vs React Navigation durante migración
WebView distribuye web móvil dentro del shell nativo. Mejor para contenido solo lectura, rara vez actualizado o legalmente alojado donde UX nativa y offline no son objetivos.
import { WebView } from "react-native-webview";
export function HelpCenterWebView({ url }: { url: string }) {
return (
<WebView
source={{ uri: url }}
startInLoadingState
sharedCookiesEnabled
// La app host debe inyectar cookie de auth o token vía injectedJavaScript
/>
);
}| Señal | WebView |
|---|---|
| Marketing / legal / help center | ✓ |
| Herramientas admin internas usadas mensualmente | ✓ |
| Checkout con restricciones PCI | ✗ (nativo o RN + SDK certificado) |
| App offline-first de campo | ✗ |
Relacionado: Autenticación compartida y Bridges - pasando sesiones en WebView y RN
Puntúa cada característica candidata 1-5 en estos ejes; el total más alto sugiere la columna.
| Eje | Incorporar RN | Reescritura | WebView |
|---|---|---|---|
| Complejidad UI | Formularios/listas altos | App completa | HTML estático |
| Necesidad offline | Requerida | Requerida | Opcional |
| Cadencia de lanzamiento | Característica semanal | Versión mayor | Cambio de copia raro |
| Capacidad de equipo nativo | Baja | Dispuesto a salir de UI nativo | Cualquiera |
| Código compartido con web | Alto | Alto | Ya en web |
| Tolerancia de riesgo | Medio (isla) | Alto (plataforma) | Bajo |
Ejemplo: "Order tracking" en una app nativa de retail
Complejidad UI: 4 | Offline: 3 | Cadencia: 5 | TS compartido: 4
- Incorporar RN (strangler fig en stack de tracking)
Ejemplo: "Terms of service"
UI: 1 | Offline: 0 | Cadencia: 1
- WebView o SFSafariViewControllerRelacionado: ADR de Adopción Incremental - decisiones clasificadas formales por escenario
Expo documenta dos formas brownfield:
| Enfoque | Ubicación RN | ¿CI nativo necesita Node? | Mejor para |
|---|---|---|---|
| Integrado | Proyecto RN envuelve o vecina ios//android/ nativo | Sí | Un equipo, cambios frecuentes en límites |
| Aislado | Repo separado/paquete monorepo - AAR + XCFramework | No (consume artefactos) | Escuadras nativas y RN separadas |
// app.config.ts - ruta aislada usa plugin de config expo-brownfield
export default {
expo: {
plugins: [
[
"expo-brownfield",
{
ios: { targetName: "CheckoutBrownfield" },
android: {
group: "com.example",
libraryName: "checkout-brownfield",
version: "2.1.0",
},
},
],
],
},
};# Escuadra RN publica artefactos; escuadra nativa consume Maven / Swift Package
npx expo-brownfield build:android --release
npx expo-brownfield build:ios --release --package CheckoutPackagenpx expo prebuild dentro del repo hostRelacionado: Descripción general de expo-brownfield - libro de recetas completo | ../native-modules/config-plugins/config-plugins.md - opciones de plugin
expo-brownfield
La app host inicializa el runtime RN una vez, luego presenta un view controller o activity.
// iOS - llama temprano en AppDelegate
import CheckoutBrownfield
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
ReactNativeHostManager.shared.initialize()
return true
}
}
// UIKit - push checkout RN
let vc = ReactNativeViewController(
moduleName: "main",
initialProps: ["cartId": cartId]
)
navigationController?.pushViewController(vc, animated: true)// Android - BrownfieldActivity + fragment
class CheckoutActivity : BrownfieldActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
showReactNativeFragment()
}
}moduleName debe coincidir con registerRootComponent / registro main de app.json en el proyecto RNinitialProps proporciona parámetros de ruta - empareja con claves de auth compartidas (próximo artículo)npx expo start); las compilaciones de lanzamiento usan el bundle incorporado dentro del artefactoRelacionado: Brownfield CI/CD - pipelines de artefactos debug vs release
Elige un flujo que sea doloroso en nativo, acotado y no en la ruta de inicio en frío crítica.
Buenas primeras porciones:
✓ Dashboard post-login (auth ya nativo)
✓ Sub-flujo de settings rara vez abierto al lanzamiento
✓ Nueva característica sin código heredado
Malas primeras porciones:
✗ Splash de lanzamiento de app / pestaña home (inicio frío + costo de init RN)
✗ Hub de navegación nativa profunda (propiedad de back-stack poco clara)
✗ Tareas de fondo / widgets (RN no en proceso)// Lado RN - escucha mensajes "open slice" nativos
import * as Brownfield from "expo-brownfield";
import { useEffect } from "react";
import { router } from "expo-router";
export function useNativeDeepLinks() {
useEffect(() => {
const sub = Brownfield.addMessageListener((event) => {
if (event.type === "OPEN_TRACKING" && event.orderId) {
router.push(`/orders/${event.orderId}`);
}
});
return () => sub.remove();
}, []);
}Brownfield.popToNative() cuando RN termina un flujo y devuelve control a UIKit/ComposeRelacionado: Autenticación compartida y Bridges - contrato
BrownfieldMessaging
Cuando PM pide "solo usa el sitio móvil", ejecuta esta comparación en una pantalla.
| Criterio | WebView | RN incorporado |
|---|---|---|
| Rendimiento de scroll en Android bajo | Entrecortado | Virtualización de lista nativa |
| Pull-to-refresh | Bridge personalizado | RefreshControl |
| Push deep link a fila | Fragilidad de URL | Ruta Expo Router tipada |
| "Funcionalidad mínima" de App Store | Riesgo si shell delgado | Presencia nativa más fuerte |
| Costo de ingeniería este trimestre | Días | Semanas |
// Compromiso: WebView para spike MVP, RN para v2 - puerta con feature flag
import { useFeatureFlag } from "@/shared/feature-flags";
export function LoyaltyScreen() {
const useNative = useFeatureFlag("loyalty_rn_v2");
return useNative ? <LoyaltyNative /> : <LoyaltyWebView uri="https://m.example.com/loyalty" />;
}Relacionado: ../architecture-design/modular-monolith-vs-multi-app/modular-monolith-vs-multi-app.md - feature flags vs apps separadas
Define quién posee cada capa antes de que llegue el primer PR.
Escuadra nativa posee:
- Lanzamiento de app, registro de push, keychain/bóveda de sesión
- Presentación de ReactNativeViewController / BrownfieldActivity
- Binarios de tienda, firma, triage de crashes nativos
Escuadra RN posee:
- Proyecto Metro, pin de Expo SDK, calidad de bundle JS
- Pantallas dentro del módulo incorporado, política OTA (si está habilitada)
- Bumps de versión de artefacto expo-brownfield
Contrato compartido (documenta en repo):
- Tipos de mensaje: OPEN_*, SESSION_*, LOGOUT
- Claves de estado compartido: auth.accessToken, auth.userId
- Semantic versioning en AAR/Maven + iOS Swift Package// Versiona el contrato del bridge - cambios de ruptura requieren bump de artefacto mayor
export const BRIDGE_CONTRACT_VERSION = "1.2.0";
export type HostToRnMessage =
| { type: "SESSION_UPDATED"; accessToken: string; userId: string }
| { type: "OPEN_CHECKOUT"; cartId: string };multipleFrameworks: true en iOS arriesgan símbolos duplicadosRelacionado: Prácticas recomendadas de Brownfield - resumen de 25 elementos | ../native-modules/autolinking-and-expo-modules-core/autolinking-and-expo-modules-core.md - autolinking en monorepos
No - Expo Go es un contenedor greenfield. Usa artefactos de debug expo-brownfield con Metro o un cliente dev integrado dentro de la app host.
Sí para el módulo RN cuando versiones de runtime se alinean - pero el binario de tienda del host aún debe distribuirse cuando cambian deps nativos. Ver Brownfield CI/CD.
RN cuando necesites offline, integración de navegación nativa o componentes compartidos con web. WebView cuando el contenido rara vez se actualiza y es propiedad de un equipo web separado sin capacidad móvil.
Uno es lo más simple. Múltiples frameworks aislados en iOS requieren multipleFrameworks: true y mangling de símbolo cuidadoso - planifica en un ADR antes de duplicar proyectos Expo.
expo-brownfieldVersiones de stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0 y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026