← Regresar a lecciones

Domina la navegación en React Native con React Navigation

¿Por qué React Navigation?
Stack Navigator: El patrón fundamental

La navegación es el sistema nervioso de cualquier aplicación móvil. Define cómo los usuarios se mueven entre pantallas, cómo se preserva el estado, y cómo se sienten las transiciones. En aplicaciones profesionales con React Native CLI, dominar la navegación no es opcional: es fundamental.

React Navigation (desarrollado por Callstack) es el estándar de facto en la industria. Potencia miles de apps en producción, incluidas apps de Fortune 500. Es la biblioteca más madura, con soporte completo para la nueva arquitectura de React Native, optimizaciones nativas, y una API declarativa que facilita mantener proyectos grandes.

En esta lección, vamos directo a lo importante: cómo usar React Navigation para construir experiencias de navegación robustas y escalables.

¿Por qué React Navigation?

En aplicaciones móviles profesionales, la navegación no es solo "cambiar de pantalla". Una biblioteca de navegación profesional debe ofrecer:

  • Gestos nativos reales: Swipe back en iOS, back button en Android
  • Animaciones de plataforma: Transiciones que respetan las guías de Material Design y Human Interface Guidelines
  • Manejo de estado persistente: El historial de navegación sobrevive a recargas y deep links
  • Tipado estricto: TypeScript para prevenir errores de navegación en tiempo de compilación
  • Compatibilidad con nueva arquitectura: Soporte completo para Fabric y Turbo Modules

React Navigation cumple todo esto y es mantenido activamente por Callstack, una empresa dedicada al ecosistema React Native.

Fundamentos: NavigationContainer

Todo árbol de navegación en React Navigation debe estar envuelto en un NavigationContainer. Este componente: Mantiene el estado de navegación, gestiona el historial completo de la app, integra deep linking y conecta la navegación con el ciclo de vida nativo.

Estructura básica:

1// App.tsx 2import { NavigationContainer } from '@react-navigation/native'; 3import AppNavigator from './navigation/AppNavigator'; 4 5export default function App() { 6 return ( 7 <NavigationContainer> 8 <AppNavigator /> 9 </NavigationContainer> 10 ); 11}

Nota: Solo necesitas un NavigationContainer en toda tu app, típicamente en el componente raíz.

Stack Navigator: El patrón fundamental

El Stack Navigator (pila) es el patrón de navegación más común. Imagina una pila de cartas: cada pantalla nueva se coloca encima, y al retroceder, se remueve la carta superior.

Native Stack vs JavaScript Stack

React Navigation ofrece dos implementaciones:

  1. @react-navigation/native-stack ✅ Recomendado: Usa UINavigationController (iOS) y Fragment (Android) nativos, animaciones 60 FPS garantizadas, menor consumo de memoria, gestos nativos por plataforma.

  2. @react-navigation/stack: Implementado completamente en JavaScript con Reanimated, más personalizable, util solo si necesitas animaciones muy específicas.

Regla de oro: Siempre usa native-stack a menos que tengas una razón muy específica para no hacerlo.

Implementación básica con TypeScript

1// src/navigation/types.ts 2export type RootStackParamList = { 3 Home: undefined; 4 Profile: { userId: string; userName: string }; 5 Settings: undefined; 6 Details: { itemId: number }; 7}; 8 9// src/navigation/RootNavigator.tsx 10import React from 'react'; 11import { createNativeStackNavigator } from '@react-navigation/native-stack'; 12import type { RootStackParamList } from './types'; 13 14import HomeScreen from '../screens/HomeScreen'; 15import ProfileScreen from '../screens/ProfileScreen'; 16 17const Stack = createNativeStackNavigator<RootStackParamList>(); 18 19export default function RootNavigator() { 20 return ( 21 <Stack.Navigator 22 initialRouteName="Home" 23 screenOptions={{ 24 headerStyle: { backgroundColor: '#6200ee' }, 25 headerTintColor: '#fff', 26 headerTitleStyle: { fontWeight: 'bold' }, 27 headerBackTitle: 'Atrás', // iOS 28 animation: 'slide_from_right', // Personalizar transición 29 }} 30 > 31 <Stack.Screen 32 name="Home" 33 component={HomeScreen} 34 options={{ title: 'Inicio' }} 35 /> 36 <Stack.Screen 37 name="Profile" 38 component={ProfileScreen} 39 options={({ route }) => ({ 40 title: route.params.userName 41 })} 42 /> 43 </Stack.Navigator> 44 ); 45}

Opciones de pantalla dinámicas

Puedes configurar opciones basadas en props o estado:

1<Stack.Screen 2 name="Profile" 3 component={ProfileScreen} 4 options={({ route, navigation }) => ({ 5 title: route.params.userName, 6 headerRight: () => ( 7 <Button 8 onPress={() => navigation.navigate('Settings')} 9 title="Config" 10 /> 11 ), 12 })} 13/>

Para navegar desde cualquier componente, usa el hook useNavigation:

1import { useNavigation } from '@react-navigation/native'; 2import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; 3import type { RootStackParamList } from '../navigation/types'; 4 5type NavigationProp = NativeStackNavigationProp<RootStackParamList>; 6 7function UserCard({ userId, userName }: Props) { 8 const navigation = useNavigation<NavigationProp>(); 9 10 const handlePress = () => { 11 // TypeScript valida los parámetros 12 navigation.navigate('Profile', { 13 userId, 14 userName 15 }); 16 }; 17 18 return ( 19 <TouchableOpacity onPress={handlePress}> 20 <Text>{userName}</Text> 21 </TouchableOpacity> 22 ); 23}

Métodos de navegación importantes

1// Navegar a una pantalla 2navigation.navigate('Profile', { userId: '123' }); 3 4// Ir atrás (pop) 5navigation.goBack(); 6 7// Volver a una pantalla específica del stack 8navigation.navigate('Home'); 9 10// Reemplazar la pantalla actual (no permite volver atrás) 11navigation.replace('Login'); 12 13// Limpiar el stack y navegar 14navigation.reset({ 15 index: 0, 16 routes: [{ name: 'Home' }], 17}); 18 19// Ir al inicio del stack 20navigation.popToTop(); 21 22// Verificar si puedes ir atrás 23if (navigation.canGoBack()) { 24 navigation.goBack(); 25}

Recibir parámetros: route.params

Desde la pantalla destino, accede a los parámetros con route.params:

1import type { NativeStackScreenProps } from '@react-navigation/native-stack'; 2import type { RootStackParamList } from '../navigation/types'; 3 4type Props = NativeStackScreenProps<RootStackParamList, 'Profile'>; 5 6function ProfileScreen({ route, navigation }: Props) { 7 const { userId, userName } = route.params; 8 9 // TypeScript garantiza que estos parámetros existen 10 return ( 11 <View> 12 <Text>Usuario: {userName}</Text> 13 <Text>ID: {userId}</Text> 14 <Button 15 title="Editar" 16 onPress={() => navigation.navigate('EditProfile', { userId })} 17 /> 18 </View> 19 ); 20}

Tab Navigator: Navegación por pestañas

El Tab Navigator es perfecto para secciones principales de tu app accesibles desde una barra inferior. Piensa en apps como Instagram, Twitter, o cualquier app con navegación persistente.

1// src/navigation/types.ts 2export type MainTabParamList = { 3 HomeTab: undefined; 4 SearchTab: undefined; 5 ProfileTab: { userId: string }; 6 SettingsTab: undefined; 7}; 8 9// src/navigation/TabNavigator.tsx 10import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 11import Icon from 'react-native-vector-icons/Ionicons'; 12 13const Tab = createBottomTabNavigator<MainTabParamList>(); 14 15export default function TabNavigator() { 16 return ( 17 <Tab.Navigator 18 screenOptions={({ route }) => ({ 19 tabBarIcon: ({ focused, color, size }) => { 20 let iconName: string; 21 22 switch (route.name) { 23 case 'HomeTab': 24 iconName = focused ? 'home' : 'home-outline'; 25 break; 26 case 'SearchTab': 27 iconName = focused ? 'search' : 'search-outline'; 28 break; 29 case 'ProfileTab': 30 iconName = focused ? 'person' : 'person-outline'; 31 break; 32 } 33 34 return <Icon name={iconName} size={size} color={color} />; 35 }, 36 tabBarActiveTintColor: '#6200ee', 37 tabBarInactiveTintColor: 'gray', 38 headerShown: false, // Ocultar header por defecto 39 })} 40 > 41 <Tab.Screen 42 name="HomeTab" 43 component={HomeScreen} 44 options={{ tabBarLabel: 'Inicio' }} 45 /> 46 <Tab.Screen 47 name="SearchTab" 48 component={SearchScreen} 49 options={{ tabBarLabel: 'Buscar' }} 50 /> 51 <Tab.Screen 52 name="ProfileTab" 53 component={ProfileScreen} 54 options={{ tabBarLabel: 'Perfil' }} 55 /> 56 </Tab.Navigator> 57 ); 58}

Ocultar tabs en pantallas específicas

A veces quieres ocultar la barra de tabs en ciertas pantallas:

1// Desde la pantalla 2useLayoutEffect(() => { 3 navigation.getParent()?.setOptions({ 4 tabBarStyle: { display: 'none' } 5 }); 6 7 return () => { 8 navigation.getParent()?.setOptions({ 9 tabBarStyle: undefined 10 }); 11 }; 12}, [navigation]);

Drawer Navigator: Menú lateral

El Drawer Navigator crea un menú lateral deslizable, común en apps con muchas secciones.

1import { createDrawerNavigator } from '@react-navigation/drawer'; 2 3type DrawerParamList = { 4 Home: undefined; 5 Profile: undefined; 6 Settings: undefined; 7}; 8 9const Drawer = createDrawerNavigator<DrawerParamList>(); 10 11export default function DrawerNavigator() { 12 return ( 13 <Drawer.Navigator 14 screenOptions={{ 15 drawerActiveTintColor: '#6200ee', 16 drawerInactiveTintColor: 'gray', 17 drawerStyle: { 18 backgroundColor: '#f8f8f8', 19 width: 280, 20 }, 21 }} 22 > 23 <Drawer.Screen 24 name="Home" 25 component={HomeScreen} 26 options={{ 27 drawerLabel: 'Inicio', 28 drawerIcon: ({ color, size }) => ( 29 <Icon name="home" size={size} color={color} /> 30 ), 31 }} 32 /> 33 <Drawer.Screen name="Profile" component={ProfileScreen} /> 34 <Drawer.Screen name="Settings" component={SettingsScreen} /> 35 </Drawer.Navigator> 36 ); 37}

Abrir/cerrar drawer programáticamente

1import { DrawerActions } from '@react-navigation/native'; 2 3function SomeScreen() { 4 const navigation = useNavigation(); 5 6 // Abrir drawer 7 const openDrawer = () => { 8 navigation.dispatch(DrawerActions.openDrawer()); 9 }; 10 11 // Cerrar drawer 12 const closeDrawer = () => { 13 navigation.dispatch(DrawerActions.closeDrawer()); 14 }; 15 16 // Toggle 17 const toggleDrawer = () => { 18 navigation.dispatch(DrawerActions.toggleDrawer()); 19 }; 20 21 return <Button title="Abrir menú" onPress={openDrawer} />; 22}

En apps complejas, es común anidar navegadores. Por ejemplo: tabs en el nivel superior, cada tab con su propio stack.

1// Cada tab tiene su propio stack 2function HomeStackNavigator() { 3 return ( 4 <Stack.Navigator> 5 <Stack.Screen name="HomeMain" component={HomeScreen} /> 6 <Stack.Screen name="Details" component={DetailsScreen} /> 7 </Stack.Navigator> 8 ); 9} 10 11function ProfileStackNavigator() { 12 return ( 13 <Stack.Navigator> 14 <Stack.Screen name="ProfileMain" component={ProfileScreen} /> 15 <Stack.Screen name="EditProfile" component={EditProfileScreen} /> 16 </Stack.Navigator> 17 ); 18} 19 20// Tab Navigator contiene los stacks 21function TabNavigator() { 22 return ( 23 <Tab.Navigator> 24 <Tab.Screen 25 name="HomeTab" 26 component={HomeStackNavigator} 27 options={{ headerShown: false }} 28 /> 29 <Tab.Screen 30 name="ProfileTab" 31 component={ProfileStackNavigator} 32 options={{ headerShown: false }} 33 /> 34 </Tab.Navigator> 35 ); 36}

Para navegar desde un stack hijo a una pantalla de otro stack:

1// Desde HomeScreen (dentro de HomeTab) 2function HomeScreen() { 3 const navigation = useNavigation(); 4 5 const goToProfile = () => { 6 // Navegar al tab Profile y luego a EditProfile 7 navigation.navigate('ProfileTab', { 8 screen: 'EditProfile', 9 params: { userId: '123' }, 10 }); 11 }; 12 13 return <Button title="Ir a Editar Perfil" onPress={goToProfile} />; 14}

Acceder al navegador padre

1// Desde una pantalla anidada 2function DetailsScreen() { 3 const navigation = useNavigation(); 4 5 // Acceder al Tab Navigator (padre) 6 const parentNavigation = navigation.getParent(); 7 8 // Cambiar de tab desde un stack hijo 9 const switchToProfileTab = () => { 10 parentNavigation?.navigate('ProfileTab'); 11 }; 12 13 return <Button title="Ir al tab Profile" onPress={switchToProfileTab} />; 14}

TypeScript avanzado: tipado completo del árbol de navegación

Para apps con navegación anidada, necesitas tipar correctamente toda la jerarquía:

1// src/navigation/types.ts 2import type { NavigatorScreenParams } from '@react-navigation/native'; 3 4// Stack dentro de HomeTab 5export type HomeStackParamList = { 6 HomeMain: undefined; 7 Details: { itemId: number }; 8}; 9 10// Stack dentro de ProfileTab 11export type ProfileStackParamList = { 12 ProfileMain: undefined; 13 EditProfile: { userId: string }; 14}; 15 16// Tab principal 17export type MainTabParamList = { 18 HomeTab: NavigatorScreenParams<HomeStackParamList>; 19 ProfileTab: NavigatorScreenParams<ProfileStackParamList>; 20 SettingsTab: undefined; 21}; 22 23// Root de toda la app 24export type RootStackParamList = { 25 Auth: undefined; 26 MainApp: NavigatorScreenParams<MainTabParamList>; 27 Modal: { modalType: 'info' | 'warning' }; 28}; 29 30// Helper para tipar useNavigation en cualquier pantalla 31declare global { 32 namespace ReactNavigation { 33 interface RootParamList extends RootStackParamList {} 34 } 35}

Con esta configuración, TypeScript autocompletará y validará toda tu navegación:

1// Desde cualquier pantalla, sin importar props 2function AnyComponent() { 3 const navigation = useNavigation(); 4 5 // ✅ Autocompletado completo 6 navigation.navigate('MainApp', { 7 screen: 'ProfileTab', 8 params: { 9 screen: 'EditProfile', 10 params: { userId: '123' }, 11 }, 12 }); 13}

Deep Linking: Navegación desde URLs

El deep linking permite abrir tu app en una pantalla específica desde una URL externa. Es esencial para:

  • Notificaciones push que abren pantallas específicas
  • Links compartidos (ej: perfil de usuario)
  • Marketing campaigns
  • Integración con otras apps

Configuración básica de deep linking

La configuración nativa ya está lista en tu plantilla. Solo necesitas configurar React Navigation:

1// App.tsx 2import { NavigationContainer } from '@react-navigation/native'; 3 4const linking = { 5 prefixes: ['myapp://', 'https://myapp.com'], 6 config: { 7 screens: { 8 Home: '', 9 Profile: 'user/:userId', 10 Details: 'item/:itemId', 11 Settings: 'settings', 12 }, 13 }, 14}; 15 16export default function App() { 17 return ( 18 <NavigationContainer linking={linking}> 19 <RootNavigator /> 20 </NavigationContainer> 21 ); 22}

URLs que funcionan con esta configuración

  • myapp:// → Home
  • myapp://user/123 → Profile con userId: '123'
  • myapp://item/456 → Details con itemId: '456'
  • https://myapp.com/user/123 → Profile con userId: '123'

Deep linking con navegación anidada

Para navegadores anidados (tabs con stacks), estructura el config jerárquicamente:

1const linking = { 2 prefixes: ['myapp://'], 3 config: { 4 screens: { 5 MainApp: { 6 screens: { 7 HomeTab: { 8 screens: { 9 HomeMain: 'home', 10 Details: 'details/:itemId', 11 }, 12 }, 13 ProfileTab: { 14 screens: { 15 ProfileMain: 'profile', 16 EditProfile: 'profile/edit', 17 }, 18 }, 19 }, 20 }, 21 Modal: 'modal/:modalType', 22 }, 23 }, 24};

URLs resultantes:

  • myapp://home → HomeTab > HomeMain
  • myapp://details/123 → HomeTab > Details
  • myapp://profile/edit → ProfileTab > EditProfile

Obtener la URL que activó la app

1import { Linking } from 'react-native'; 2 3function SomeScreen() { 4 useEffect(() => { 5 // Obtener URL inicial (cuando la app estaba cerrada) 6 Linking.getInitialURL().then((url) => { 7 if (url) { 8 console.log('App abierta desde:', url); 9 } 10 }); 11 12 // Escuchar deep links cuando la app está en background 13 const subscription = Linking.addEventListener('url', ({ url }) => { 14 console.log('Deep link recibido:', url); 15 }); 16 17 return () => subscription.remove(); 18 }, []); 19}

React Navigation soporta query params automáticamente:

1// URL: myapp://profile?userId=123&tab=posts 2function ProfileScreen({ route }) { 3 const { userId, tab } = route.params; 4 // userId: '123', tab: 'posts' 5}

Configuración global del NavigationContainer

Interceptar todas las transiciones de estado

1import { NavigationContainer } from '@react-navigation/native'; 2 3function App() { 4 const navigationRef = useRef(null); 5 6 return ( 7 <NavigationContainer 8 ref={navigationRef} 9 onStateChange={(state) => { 10 // Analytics: trackear cada cambio de pantalla 11 const currentRoute = navigationRef.current?.getCurrentRoute(); 12 console.log('Navegó a:', currentRoute?.name); 13 14 // Enviar a Firebase Analytics, Segment, etc. 15 analytics.logScreenView({ 16 screen_name: currentRoute?.name, 17 screen_class: currentRoute?.name, 18 }); 19 }} 20 onReady={() => { 21 console.log('Navegación lista'); 22 routingInstrumentation.registerNavigationContainer(navigationRef); 23 }} 24 > 25 <RootNavigator /> 26 </NavigationContainer> 27 ); 28}

Obtener pantalla actual desde cualquier lugar

1// Crear referencia global 2export const navigationRef = createNavigationContainerRef(); 3 4// En App.tsx 5<NavigationContainer ref={navigationRef}> 6 ... 7</NavigationContainer> 8 9// Usar desde cualquier archivo (incluso fuera de componentes) 10import { navigationRef } from './navigation/navigationRef'; 11 12export function navigateFromAnywhere(name, params) { 13 if (navigationRef.isReady()) { 14 navigationRef.navigate(name, params); 15 } 16} 17 18// Útil para: 19// - Navegación desde notificaciones push 20// - Navegación desde middleware de Redux 21// - Navegación desde servicios

Optimización de performance

Lazy loading de pantallas

Para reducir el tiempo de carga inicial, carga pantallas bajo demanda:

1import React, { Suspense } from 'react'; 2 3// Lazy load 4const ProfileScreen = React.lazy(() => import('./screens/ProfileScreen')); 5const SettingsScreen = React.lazy(() => import('./screens/SettingsScreen')); 6 7function RootNavigator() { 8 return ( 9 <Stack.Navigator> 10 <Stack.Screen name="Home" component={HomeScreen} /> 11 12 <Stack.Screen name="Profile"> 13 {(props) => ( 14 <Suspense fallback={<LoadingScreen />}> 15 <ProfileScreen {...props} /> 16 </Suspense> 17 )} 18 </Stack.Screen> 19 20 <Stack.Screen name="Settings"> 21 {(props) => ( 22 <Suspense fallback={<LoadingScreen />}> 23 <SettingsScreen {...props} /> 24 </Suspense> 25 )} 26 </Stack.Screen> 27 </Stack.Navigator> 28 ); 29}

¿Cuándo usar lazy loading? Pantallas poco frecuentes (configuración avanzada, términos legales), pantallas con dependencias pesadas, módulos de features opcionales.

Optimización de re-renders

React Navigation re-renderiza pantallas en ciertos casos. Optimiza con React.memo:

1import React, { memo } from 'react'; 2 3const HomeScreen = memo(function HomeScreen({ route, navigation }) { 4 // Este componente solo se re-renderiza si sus props cambian 5 return <View>...</View>; 6}); 7 8export default HomeScreen;

detachInactiveScreens

Por defecto, React Navigation mantiene montadas las pantallas inactivas. Para liberar memoria:

1<Stack.Navigator 2 screenOptions={{ 3 detachInactiveScreens: true, // Desmontar pantallas inactivas 4 }} 5> 6 ... 7</Stack.Navigator>

⚠️ Cuidado: Esto desmonta el componente al salir, perdiendo estado local. Úsalo solo si el estado está en Redux/Context o se persiste.