Plan de estudio intensivo · Un día

React Native 0.84
Dominio Total

Ciclos de vida · Activity/Screen lifecycle · Monorepo · Clean Architecture · iOS & Android · Todo lo que necesitas dominar hoy.

RN 0.84 Core Lifecycle Android Lifecycle iOS Monorepo Clean Architecture Kill Order New Architecture
comenzar

Tu día de batalla

8 bloques de estudio. Sin saltarte ninguno. El orden importa.

07:00 – 08:30
RN 0.84 + New Architecture
Hermes, JSI, TurboModules, Fabric. Base conceptual.
08:30 – 10:00
Android Activity Lifecycle
onCreate → onDestroy. Cómo RN se monta sobre Activity.
10:00 – 11:30
iOS App & VC Lifecycle
AppDelegate, SceneDelegate, UIViewController, SwiftUI lifecycle.
11:30 – 13:00
Screen Lifecycle en RN
useEffect, AppState, react-navigation events, focus/blur.
13:00 – 13:30
☕ Pausa / repaso
Revisar apuntes de mañana. El cerebro necesita consolidar.
13:30 – 15:00
Kill Order & Memory
Por qué matar un archivo antes que otro. iOS vs Android.
15:00 – 17:00
Monorepo
Turborepo + pnpm workspaces. Estructura, build, CI.
17:00 – 20:00
Clean Architecture
Domain, Data, Presentation. Use Cases, Repositories.

React Native 0.84

Esta versión consolida la New Architecture como default. Entender sus capas es crítico para todo lo demás.

Hermes Engine

Motor JS optimizado para mobile. Pre-compila a bytecode, arranque más rápido, menor uso de RAM. En RN 0.84 es el engine por defecto tanto en Android como iOS.

Bytecode Lazy Compilation
🌉

JSI (JS Interface)

Reemplaza el viejo Bridge. Permite que JS llame directamente a código nativo C++ sin serialización JSON. Comunicación síncrona y de baja latencia.

Sin Bridge C++ Binding
🔧

TurboModules

Módulos nativos bajo JSI. Lazy loading: solo se cargan cuando se usan. Reemplazan a los NativeModules clásicos. Tipado fuerte con CodeGen.

Lazy Load CodeGen
🎨

Fabric Renderer

Nuevo sistema de renderizado. Calcula el layout en C++ (Yoga), monta vistas nativas sincrónicamente. Elimina el cuello de botella del Bridge en UI.

Yoga C++ Sync Rendering

Arquitectura: Old Bridge vs New Architecture

❌ OLD BRIDGE (antes de 0.71)

// Comunicación asíncrona via JSON
// JS Thread ←→ Bridge ←→ Native Thread
// Serialización costosa en cada llamada

JS → serialize(JSON) → queue → Native
Native → serialize(JSON) → queue → JS

// PROBLEMA: No puedes hacer esto síncronamente
nativeModule.getValue() // siempre async

✅ NEW ARCH (RN 0.84 default)

// JSI: llamadas directas sin serialización
// JS ←→ C++ ←→ Native (mismo proceso)

JS → JSI → C++ host object → Native
Native → C++ → JS // síncrono posible

// BENEFICIO: llamadas síncronas posibles
const value = turboModule.getValueSync()
💡
¿Por qué importa esto para el lifecycle? Con la New Architecture, los eventos del lifecycle nativo (onResume, viewDidAppear) pueden comunicarse a JS sincrónicamente, reduciendo el tiempo en que el estado de JS está "desincronizado" del estado real de la app.

Novedades clave en 0.84

Feature Qué cambió Impacto
New Arch por defecto Ya no es opt-in, es la base Todos tus módulos nativos deben ser TurboModules
React 18 Concurrent features habilitadas Suspense, useTransition disponibles
Metro bundler Soporte native ESM, symlinks Mejor soporte para monorepo
StyleSheet improvements Processamiento en C++ Layouts más rápidos
Expo SDK 51+ Compatible con New Arch Puedes usar Expo en proyectos New Arch

Android Activity Lifecycle

Una Activity es la ventana que aloja tu app RN en Android. Entender su ciclo define cuándo tu JS está vivo, pausado o destruido.

Diagrama de estados

App lanzada
Usuario toca el ícono
onCreate()
Inicialización. Se crea el bundle RN aquí
onStart()
Activity visible, JS montando componentes
▶ onResume() — RUNNING
App en primer plano. Usuario interactúa
onPause()
Otra app encima. Guardar estado rápido aquí
onStop()
No visible. Liberar recursos pesados
onDestroy()
Activity destruida. Liberar TODO

Cómo RN se conecta

class MainActivity : ReactActivity() {

  override fun onCreate(saved: Bundle?) {
    super.onCreate(saved)
    // RN crea el ReactRootView aquí
    // Hermes comienza a ejecutar JS
    // Se monta el componente raíz
  }

  override fun onResume() {
    super.onResume()
    // AppState → "active" en JS
    // Se disparan los listeners de focus
  }

  override fun onPause() {
    super.onPause()
    // AppState → "background" en JS
    // Pausar animaciones, timers
  }

  override fun onDestroy() {
    super.onDestroy()
    // ReactInstanceManager se destruye
    // JS context finalizado
  }
}
⚠️
onPause vs onStop onPause debe ser ultra-rápido (no hagas I/O). onStop es donde puedes hacer operaciones más pesadas de limpieza. RN mapea ambos a AppState "background".

Back Stack

Android mantiene un stack de Activities. Cuando el usuario navega "atrás", la Activity superior se destruye. En RN con una sola Activity, la navegación está en JS, no en el back stack nativo.

Configuration Changes (rotación, idioma)

<!-- Sin esto, la Activity se destruye y recrea al rotar -->
<activity
  android:name=".MainActivity"
  android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|
                          screenSize|smallestScreenSize|uiMode">
  <!-- RN maneja estos cambios internamente sin recrear la Activity -->
</activity>

iOS App & ViewController Lifecycle

App Lifecycle (AppDelegate)

application(_:didFinishLaunching)
RN bridge/JSI se inicializa aquí
applicationDidBecomeActive
AppState → "active"
applicationWillResignActive
Call incoming, Control Center abierto
applicationDidEnterBackground
AppState → "background". Tienes ~5 seg
applicationWillTerminate
App siendo terminada. Sin garantía de tiempo

ViewController Lifecycle

viewDidLoad
Una vez. View cargada en memoria
viewWillAppear
Antes de mostrarse. Configura datos
viewDidAppear ← focus en RN
Visible. Inicia animaciones, fetch data
viewWillDisappear
Próximo a ocultarse. Cancela operaciones
viewDidDisappear ← blur en RN
Ya no visible. Limpieza
💡
Scene-based lifecycle (iOS 13+) Con SceneDelegate, cada "ventana" tiene su propio ciclo. RN 0.84 soporta múltiples scenes. Los métodos son similares pero en UISceneDelegate.

AppDelegate en RN 0.84

@UIApplicationMain
class AppDelegate: RCTAppDelegate {

  override func application(_ application: UIApplication,
    didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    // RN 0.84: Nueva forma de setup con New Architecture
    self.moduleName = "MyApp"
    self.initialProps = [:]

    return super.application(application, didFinishLaunchingWithOptions: options)
    // super crea el RCTHost (New Arch) o RCTBridge (legacy)
  }

  override func bundleURL() -> URL? {
    // En debug: Metro bundler
    // En prod: bundle local
    return RCTBundleURLProvider.sharedSettings()
      .jsBundleURL(forBundleRoot: "index")
  }
}

Screen Lifecycle en React Native

En RN no tienes Activities por pantalla. La navegación es en JS. El lifecycle de pantalla lo construyes con hooks + eventos del navigator.

🔁

useEffect — El lifecycle de componentes

const MyScreen = () => {

  useEffect(() => {
    // ← MOUNT (como componentDidMount)
    fetchData()
    const sub = subscribe()

    return () => {
      // ← UNMOUNT (como componentWillUnmount)
      sub.unsubscribe()
    }
  }, []) // [] = solo mount/unmount

  useEffect(() => {
    // ← Cuando cambia userId
    loadUser(userId)
  }, [userId]) // dep array
}
📱

AppState — Estado de la app

import { AppState } from 'react-native'

const useAppState = () => {
  const [state, setState] = useState(
    AppState.currentState
  )

  useEffect(() => {
    const sub = AppState.addEventListener(
      'change',
      nextState => {
        // 'active' | 'background' | 'inactive'
        if (nextState === 'active') {
          // Volvemos al primer plano → refresh
        }
        setState(nextState)
      }
    )
    return () => sub.remove()
  }, [])

  return state
}
🗺️

react-navigation — Screen focus

import { useIsFocused, useFocusEffect }
  from '@react-navigation/native'

// Opción 1: hook simple
const isFocused = useIsFocused()

// Opción 2: efecto en focus (RECOMENDADO)
useFocusEffect(
  useCallback(() => {
    // ← Cuando la pantalla recibe focus
    fetchFreshData()

    return () => {
      // ← Cuando la pantalla pierde focus
      cancelRequests()
    }
  }, [])
)

// Opción 3: eventos directos
navigation.addListener('focus', () => {})
navigation.addListener('blur', () => {})

Mapa completo: Nativo → JS

Evento Nativo Android Evento Nativo iOS En React Native JS Cuándo usarlo
onCreate didFinishLaunching Montaje del componente raíz Setup inicial, providers
onResume applicationDidBecomeActive AppState → "active" Refresh data, reanudar timers
onPause applicationWillResignActive AppState → "inactive" Pausar video/audio
onStop didEnterBackground AppState → "background" Guardar estado, cancelar fetch
onDestroy willTerminate Desmontaje + GC Limpieza final (sin garantía en iOS)
Activity.onResume viewDidAppear navigation focus event Actualizar pantalla al volver
Activity.onPause viewDidDisappear navigation blur event Cancelar operaciones de la pantalla

Kill Order: Por qué matar un archivo antes que otro

En mobile, la memoria y el ciclo de vida de procesos define cuándo el SO mata partes de tu app. Entender el orden es crítico para no perder datos y evitar crashes.

Android — Jerarquía de procesos

Android usa una jerarquía de importancia de procesos. El LMK (Low Memory Killer) los mata en este orden cuando hay presión de memoria:

1. Foreground Process
App visible, usuario interactuando. NUNCA se mata
2. Visible Process
No focused pero visible (dialog). Raramente se mata
3. Service Process
Background service activo. Se mata si hay presión
4. Cached Process
App en background reciente. Se mata fácilmente
5. Empty Process
Sin actividad. Primera víctima del LMK ← SE MATA PRIMERO

iOS — Memoria y Jetsam

iOS usa Jetsam (su propio memory killer). El orden de kill es diferente:

1. Suspended apps (más antigua)
Sin notificación. Muertas silenciosamente. ← PRIMERAS
2. Background apps (mayor memoria)
Las que usan más RAM en background
3. Background tasks
Fetch, push handling. Si exceden tiempo/memoria
4. Foreground app
Solo si tiene memory leak grave. Crash visible
⚠️
En iOS NO hay willTerminate confiable Cuando Jetsam mata tu app, applicationWillTerminate puede NO llamarse. Guarda estado crítico en applicationDidEnterBackground (tienes ~5 segundos).

Kill Order aplicado a archivos / recursos en RN

En el contexto de desarrollo, "matar un archivo antes que otro" también refiere al orden en que debes limpiar recursos cuando una pantalla/componente muere:

const Screen = () => {
  useEffect(() => {
    const networkReq = startRequest()
    const timer      = setInterval(poll, 5000)
    const sub        = eventEmitter.addListener('event', cb)
    const animation  = startAnimation()

    return () => {
      // ✅ ORDEN CORRECTO DE CLEANUP:

      // 1. PRIMERO: Cancela operaciones async
      //    Evita que un callback llegue tras el unmount
      networkReq.cancel()

      // 2. Limpia timers (pueden disparar callbacks)
      clearInterval(timer)

      // 3. Remueve event listeners
      //    Evita memory leaks y callbacks fantasma
      sub.remove()

      // 4. ÚLTIMO: Detén animaciones
      //    Animations son refs, no causan re-renders
      animation.stop()
    }
  }, [])
}
Regla de oro del Kill Order Mata primero lo que puede generar efectos secundarios (callbacks, setState tras unmount). Luego mata los recursos pasivos (animaciones, refs). Si un callback llega después del unmount, React te lanzará un warning y puedes tener bugs sutiles de estado.

Monorepo con React Native

Un monorepo es un único repositorio que aloja múltiples packages/apps. En RN, es la estructura estándar para compartir código entre app móvil, web, y packages.

Estructura recomendada

my-monorepo/
├── apps/
│   ├── mobile/          # React Native app
│   │   ├── android/
│   │   ├── ios/
│   │   ├── src/
│   │   └── package.json
│   └── web/             # Next.js / Expo Web
│       └── package.json
├── packages/
│   ├── ui/              # Componentes compartidos
│   │   └── package.json  # "@myapp/ui"
│   ├── core/            # Business logic
│   │   └── package.json  # "@myapp/core"
│   └── types/           # TypeScript types
│       └── package.json  # "@myapp/types"
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

pnpm-workspace.yaml

packages:
  - 'apps/*'
  - 'packages/*'

turbo.json

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "lint": {}
  }
}

Metro config para monorepo

const { getDefaultConfig } = require(
  '@react-native/metro-config'
)
const path = require('path')

const projectRoot = __dirname
const workspaceRoot = path.resolve(projectRoot, '../..')

const config = getDefaultConfig(projectRoot)

// CRÍTICO: Metro debe conocer todos los packages
config.watchFolders = [workspaceRoot]

config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, 'node_modules'),
  path.resolve(workspaceRoot, 'node_modules'),
]

module.exports = config
⚠️
Symlinks en RN 0.84 Metro 0.81+ (incluido en RN 0.84) soporta symlinks nativamente. Antes tenías que usar resolvers custom. Asegúrate de tener resolver.unstable_enableSymlinks: true si usas versiones anteriores.

Flujo de trabajo con Turborepo

# Instalar desde raíz del monorepo
pnpm install

# Buildear todos los packages en orden correcto
pnpm turbo build

# Correr solo el mobile
pnpm turbo dev --filter=mobile

# Agregar dependencia a un package específico
pnpm add react-query --filter=@myapp/core

# Agregar package local como dependencia
pnpm add @myapp/ui --filter=mobile
# En package.json de mobile: "@myapp/ui": "workspace:*"

Clean Architecture en React Native

Clean Architecture separa el código en capas con dependencias que siempre apuntan hacia adentro. El dominio no sabe nada de React, ni de la API, ni de la base de datos.

Las 3 capas fundamentales

📱 PRESENTATION LAYER
Screens · Components · ViewModels · Hooks
↓ llama a ↓
🧠 DOMAIN LAYER (el corazón)
Entities · Use Cases · Repository Interfaces
↓ implementado por ↓
💾 DATA LAYER
API Services · Local DB · Repository Implementations
Las dependencias SIEMPRE apuntan hacia adentro (hacia Domain)
📱

Presentation Layer

Todo lo que el usuario ve y toca. No tiene lógica de negocio. Llama Use Cases y recibe resultados para mostrar.

Screens Hooks Redux/Zustand
🧠

Domain Layer

Reglas de negocio puras. Sin React, sin fetch, sin AsyncStorage. Solo TypeScript puro. Es la capa más estable.

Entities Use Cases Interfaces
💾

Data Layer

Implementa las interfaces del Domain. Puede ser REST API, GraphQL, SQLite, AsyncStorage. Intercambiable sin afectar el resto.

Repositories DTOs Mappers

Ejemplo completo: Feature "Login"

// DOMAIN: La entidad. Puro TypeScript.
export interface User {
  id: string
  email: string
  name: string
  role: 'admin' | 'user'
}
// DOMAIN: Interface (contrato). No sabe CÓMO se implementa.
export interface AuthRepository {
  login(email: string, password: string): Promise<User>
  logout(): Promise<void>
  getCurrentUser(): Promise<User | null>
}
// DOMAIN: Use Case. Orquesta la lógica de negocio.
export class LoginUseCase {
  constructor(private authRepo: AuthRepository) {}

  async execute(email: string, password: string): Promise<User> {
    // Validaciones de negocio
    if (!email.includes('@')) throw new Error('Email inválido')
    if (password.length < 8)   throw new Error('Password muy corto')

    return this.authRepo.login(email, password)
  }
}
// DATA: Implementación concreta. Aquí sí hablamos con la API.
export class AuthRepositoryImpl implements AuthRepository {
  async login(email: string, password: string): Promise<User> {
    const response = await fetch(`${API_URL}/auth/login`, {
      method: 'POST',
      body: JSON.stringify({ email, password })
    })
    const dto = await response.json()
    return UserMapper.toDomain(dto) // DTO → Entity
  }
}
// PRESENTATION: La pantalla. Solo UI + llamar use case.
const useLoginViewModel = () => {
  const loginUseCase = useDI<LoginUseCase>('LoginUseCase')
  const [loading, setLoading] = useState(false)
  const [error, setError]     = useState<string>()

  const login = async (email: string, pass: string) => {
    setLoading(true)
    try {
      await loginUseCase.execute(email, pass)
    } catch (e) {
      setError(e.message)
    } finally { setLoading(false) }
  }

  return { login, loading, error }
}

const LoginScreen = () => {
  const { login, loading, error } = useLoginViewModel()
  // ... solo JSX aquí
}

Estructura de carpetas en el monorepo

packages/core/src/
├── domain/
│   ├── entities/       # User, Product, Order...
│   ├── repositories/   # Interfaces (contratos)
│   └── usecases/       # LoginUseCase, GetProductsUseCase...
├── data/
│   ├── datasources/    # API calls, SQLite queries
│   ├── repositories/   # Implementaciones concretas
│   └── mappers/        # DTO ↔ Entity conversions
└── di/
    └── container.ts    # Dependency Injection setup

apps/mobile/src/
└── presentation/
    ├── screens/        # Pantallas
    ├── components/     # Componentes UI
    └── viewmodels/     # Hooks con lógica de UI

Cheatsheet Maestro

Concepto La pregunta clave La respuesta de 1 línea
JSI vs Bridge ¿Cómo JS habla con nativo? Bridge = JSON async lento. JSI = C++ directo, puede ser síncrono.
TurboModules ¿Qué reemplaza a NativeModules? Módulos nativos lazy-loaded con tipado fuerte generado por CodeGen.
Fabric ¿Qué reemplaza al UIManager? Renderer C++ con Yoga que puede actualizar vistas sincrónicamente.
Activity Lifecycle ¿Cuándo guardar estado en Android? En onPause() para datos críticos. onStop() para operaciones más lentas.
iOS Background ¿Cuánto tiempo tienes en background? ~5 segundos en applicationDidEnterBackground. Pide más con background tasks.
Kill Order Android ¿A quién mata primero el LMK? Empty → Cached → Service → Visible → Foreground (de primero a último en morir).
Kill Order iOS ¿A quién mata primero Jetsam? Apps suspendidas más antiguas → las que usan más RAM en background.
Cleanup Order RN ¿Qué matar primero en useEffect cleanup? Primero cancelar async requests, luego timers, luego listeners, luego animaciones.
useFocusEffect vs useEffect ¿Cuándo re-ejecutar al volver a una pantalla? useEffect con [] solo se ejecuta una vez. useFocusEffect se ejecuta cada vez que la pantalla recibe focus.
AppState ¿Cómo detectar app en background desde JS? AppState.addEventListener('change', cb) retorna 'active' | 'background' | 'inactive'.
Monorepo Metro ¿Qué config es obligatoria? watchFolders debe incluir la raíz del workspace para que Metro vea los packages.
Domain Layer ¿Qué NO puede importar? Nada de React, nada de fetch, nada de DB. Solo TypeScript puro.
Use Case ¿Qué hace exactamente? Orquesta la lógica de negocio llamando a una o más interfaces de Repository.
Repository Pattern ¿Por qué una interface y una implementación? La interface está en Domain (estable), la implementación en Data (intercambiable). Puedes testear con mocks.
🏆
Mañana: ¿Cómo saber que lo dominaste? Puedes responder: ¿Por qué una app RN no tiene múltiples Activities en Android? ¿Qué pasa si haces setState después del unmount? ¿Por qué el Domain no puede importar de Data? ¿Qué diferencia hay entre useEffect([]) y useFocusEffect? Si respondes estas 4 sin dudar, lo dominaste.