Typescript
react native
React Navigation
Desarrollo Móvil
Callstack
En esta lección, vamos directo a lo importante: cómo usar React Navigation para construir experiencias de navegación robustas y escalables.
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}
@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.
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}
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/>
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}
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}
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}
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}
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]);
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}
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}
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}
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}
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}
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}
El deep linking permite abrir tu app en una pantalla específica desde una URL externa. Es esencial para:
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}
myapp:// → Homemyapp://user/123 → Profile con userId: '123'myapp://item/456 → Details con itemId: '456'https://myapp.com/user/123 → Profile con userId: '123'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 > HomeMainmyapp://details/123 → HomeTab > Detailsmyapp://profile/edit → ProfileTab > EditProfile1import { 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}
1// URL: myapp://profile?userId=123&tab=posts 2function ProfileScreen({ route }) { 3 const { userId, tab } = route.params; 4 // userId: '123', tab: 'posts' 5}
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}
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
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.
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;
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.