Ciclos de vida · Activity/Screen lifecycle · Monorepo · Clean Architecture · iOS & Android · Todo lo que necesitas dominar hoy.
8 bloques de estudio. Sin saltarte ninguno. El orden importa.
Esta versión consolida la New Architecture como default. Entender sus capas es crítico para todo lo demás.
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.
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.
Módulos nativos bajo JSI. Lazy loading: solo se cargan cuando se usan. Reemplazan a los NativeModules clásicos. Tipado fuerte con CodeGen.
Nuevo sistema de renderizado. Calcula el layout en C++ (Yoga), monta vistas nativas sincrónicamente. Elimina el cuello de botella del Bridge en UI.
❌ 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()
| 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 |
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.
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 } }
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.
<!-- 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>
@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") } }
En RN no tienes Activities por pantalla. La navegación es en JS. El lifecycle de pantalla lo construyes con hooks + eventos del navigator.
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 }
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 }
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', () => {})
| 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 |
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 usa una jerarquía de importancia de procesos. El LMK (Low Memory Killer) los mata en este orden cuando hay presión de memoria:
iOS usa Jetsam (su propio memory killer). El orden de kill es diferente:
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() } }, []) }
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.
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
packages: - 'apps/*' - 'packages/*'
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["^build"]
},
"lint": {}
}
}
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
resolver.unstable_enableSymlinks: true si usas
versiones anteriores.
# 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 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.
Todo lo que el usuario ve y toca. No tiene lógica de negocio. Llama Use Cases y recibe resultados para mostrar.
Reglas de negocio puras. Sin React, sin fetch, sin AsyncStorage. Solo TypeScript puro. Es la capa más estable.
Implementa las interfaces del Domain. Puede ser REST API, GraphQL, SQLite, AsyncStorage. Intercambiable sin afectar el resto.
// 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í }
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
| 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. |