---
name: mobile-react-native
description: React Native + Expo mejores prácticas. Cubre navegación con React Navigation y Expo Router, state management (Zustand, Redux Toolkit), native modules y Turbo Modules, animaciones con Reanimated, styling con StyleSheet y NativeWind, performance con Hermes y FlashList, testing con Detox y Jest Testing Library.
version: "1.0.0"
herramientasPermitidas: [Read]
exclusiones:
  - "No cargar para Flutter (Dart, Riverpod, GoRouter, widgets) — React Native usa JavaScript/TypeScript con un puente nativo diferente; para Flutter cargar `mobile-flutter`."
  - "No cargar para React web (Next.js, Server Components, CSS) — React Native no tiene DOM, no usa HTML/CSS y las APIs de navegación son distintas; para web cargar `react-experto`."
  - "No cargar para errores de compilación de módulos nativos, Xcode o Gradle en React Native — para build errors nativos usar las herramientas de diagnóstico de la plataforma."
  - "No cargar para apps iOS nativas (Swift) o Android nativas (Kotlin) — React Native y los SDKs nativos tienen modelos de ciclo de vida completamente distintos."
evolvable: true  # default para skill estandar
---
# React Native + Expo Experto

## Cuándo NO cargar

- El proyecto es Flutter (Dart) — React Native usa JavaScript con puente nativo; para Flutter cargar `mobile-flutter`.
- El contexto es React para web (Next.js, Server Components, DOM) — React Native no tiene DOM ni HTML/CSS; para web cargar `react-experto`.
- Los errores son de compilación nativa: Xcode build failures, Gradle errors, módulos nativos no encontrados — usar las herramientas de diagnóstico de iOS/Android.
- El proyecto es una app nativa pura en Swift o Kotlin — cargar `swift-experto` o `kotlin-experto`.

## Filosofía

React Native comparte la lógica de JavaScript con React web, pero el modelo
de renderizado es completamente nativo. El puente JS-nativo tiene costo;
la arquitectura moderna (Nueva Arquitectura / Fabric) lo reduce con JSI,
pero las reglas de rendimiento siguen siendo críticas.

---

## 1. Navegación — Expo Router (recomendado para proyectos nuevos)

Expo Router usa el sistema de archivos como router, similar a Next.js App Router:

```
app/
├── _layout.tsx           # Root layout (Stack/Tabs)
├── index.tsx             # / (pantalla home)
├── (tabs)/
│   ├── _layout.tsx       # Tabs layout
│   ├── inicio.tsx        # /inicio
│   └── perfil.tsx        # /perfil
├── usuarios/
│   ├── index.tsx         # /usuarios
│   └── [id].tsx          # /usuarios/:id
└── modal.tsx             # Pantalla modal
```

```tsx
// app/_layout.tsx — Root layout con Stack
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerStyle: { backgroundColor: '#f8fafc' } }}>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
    </Stack>
  );
}

// Navegación programática
import { router } from 'expo-router';
router.push('/usuarios/123');
router.replace('/login');
router.back();
```

---

## 2. State Management — Zustand (recomendado)

```typescript
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface SesionStore {
  usuario: Usuario | null;
  token: string | null;
  login: (usuario: Usuario, token: string) => void;
  logout: () => void;
}

export const useSesionStore = create<SesionStore>()(
  persist(
    (set) => ({
      usuario: null,
      token: null,
      login: (usuario, token) => set({ usuario, token }),
      logout: () => set({ usuario: null, token: null }),
    }),
    { name: 'sesion-storage', storage: createJSONStorage(() => AsyncStorage) }
  )
);
```

---

## 3. Animaciones con Reanimated

```tsx
import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming } from 'react-native-reanimated';

function TarjetaAnimada() {
  const escala = useSharedValue(1);

  const estiloAnimado = useAnimatedStyle(() => ({
    transform: [{ scale: escala.value }],
  }));

  return (
    <Animated.View style={[styles.tarjeta, estiloAnimado]}>
      <Pressable
        onPressIn={() => { escala.value = withSpring(0.95); }}
        onPressOut={() => { escala.value = withSpring(1); }}
      >
        <Text>Presiona</Text>
      </Pressable>
    </Animated.View>
  );
}
```

---

## 4. Styling: StyleSheet (enfoque nativo)

```tsx
// Crear estilos fuera del componente (procesados UNA vez, no en cada render)
const styles = StyleSheet.create({
  contenedor: { flex: 1, backgroundColor: '#f8fafc', padding: 16 },
  tarjeta: {
    backgroundColor: '#ffffff', borderRadius: 12, padding: 16, marginBottom: 12,
    shadowColor: '#000', shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.05, shadowRadius: 4, elevation: 2,
  },
});
```

---

## 5. Performance

- Hermes habilitado por defecto en Expo SDK 48+: `"jsEngine": "hermes"`
- Usar `FlashList` de `@shopify/flash-list` en lugar de `FlatList` para listas grandes
- SIEMPRE pasar `estimatedItemSize` a FlashList
- SIEMPRE memorizar `renderItem` con `useCallback` y componentes de lista con `React.memo`

---

## 6. Anti-patrones Críticos

- **Bridge overhead**: usar `useWindowDimensions()` hook en vez de `Dimensions.get('window')` en cada render
- **Bloquear hilo JS**: usar librerías nativas para procesamiento pesado (ImageManipulator, etc.)
- **Estilos inline en listas**: NUNCA `style={{ ... }}` en componentes de lista, usar `StyleSheet.create` fuera
- **setInterval sin cleanup**: SIEMPRE retornar cleanup en `useEffect`

---

## Ejemplos Completos

Para implementaciones de React Navigation tipada, Redux Toolkit, Turbo Modules,
Expo Modules API, Gesture Handler + Reanimated, NativeWind v4, y testing con
Jest/RNTL y Detox, ver
[recursos/ejemplos-completos.md](recursos/ejemplos-completos.md).

## Gotchas / Errores comunes no obvios

**`useEffect` con `async` function directa no maneja el cleanup correctamente y puede causar memory leaks**: `useEffect(async () => { await fetch(...); }, [])` no puede retornar una función de cleanup desde una función async (retorna una Promise, no una función). Causa: `useEffect` espera que el callback retorne una función de cleanup o `undefined` — retornar una Promise hace que React ignore el cleanup. Fix: declarar la función async dentro del efecto y llamarla: `useEffect(() => { const fn = async () => { ... }; fn(); return () => { /* cleanup */ }; }, [])`.

**Reanimated `useAnimatedStyle` que accede a valores de estado React (no shared values) no actualiza en el UI thread**: `useAnimatedStyle(() => ({ opacity: someReactState }))` no reacciona cuando `someReactState` cambia porque los animated styles se ejecutan en el UI thread y no tienen acceso al estado React del JS thread. Causa: la separación de threads de Reanimated 2/3. Fix: convertir el valor a `useSharedValue` y actualizarlo con `useEffect` cuando cambie el estado React: `useEffect(() => { sharedVal.value = someReactState; }, [someReactState])`.

**`FlashList` con items de altura variable sin `overrideItemLayout` tiene scroll jumpy**: `FlashList` usa `estimatedItemSize` para todos los items por defecto — si los items tienen altura muy variable (ej: mensajes de chat), el scroll puede saltar al navegar de vuelta a items ya renderizados. Causa: la estimación de tamaño acumula error en listas con alta variabilidad. Fix: implementar `overrideItemLayout` que retorna el tamaño real de cada item conocido, o medir los items con `onLayout` y almacenar sus tamaños para retornarlos en la función.

**`Zustand` con `persist` en AsyncStorage puede devolver `null` en el primer render antes de que se rehidrate**: `useSesionStore(s => s.usuario)` puede retornar `null` el primer frame aunque haya datos persistidos, porque la rehidratación de AsyncStorage es asíncrona. Causa: AsyncStorage es asíncrono — el store se inicializa con el estado por defecto y luego se rehidrata. Fix: verificar el estado de hidratación con `useStore(s => s._hasHydrated)` (habilitando el `onRehydrateStorage` callback) o usar `zustand/middleware/persist` con la opción `skipHydration: true` y disparar la hidratación manualmente en el splash screen.
