Navegación profesional en React Native

Capítulo 3

Tiempo estimado de lectura: 9 minutos

+ Ejercicio

¿Por qué la navegación “profesional” importa?

En una app real, la navegación no es solo “ir de A a B”: define la arquitectura de pantallas, cómo se comparten parámetros, cómo se controlan headers, cómo se integran enlaces externos (deep links) y cómo se organizan flujos (auth, onboarding) sin duplicar lógica. En React Native, el estándar de facto es React Navigation, que ofrece patrones comunes: stack, tabs y drawer, además de herramientas para estados de foco y ciclo de vida de pantallas.

Instalación y configuración base (paso a paso)

1) Instala dependencias principales

En un proyecto React Native (CLI o Expo), instala React Navigation y dependencias nativas:

npm install @react-navigation/native

Dependencias requeridas por React Navigation:

npm install react-native-screens react-native-safe-area-context

Si usas navegación tipo stack nativa:

npm install @react-navigation/native-stack

Si usas tabs:

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

npm install @react-navigation/bottom-tabs

Si usas drawer:

npm install @react-navigation/drawer react-native-gesture-handler react-native-reanimated

2) Envuelve tu app con NavigationContainer

Este contenedor mantiene el estado de navegación y habilita linking, temas y listeners globales.

import { NavigationContainer } from '@react-navigation/native';

export default function App() {
  return (
    <NavigationContainer>
      {/* Tu árbol de navegadores */}
    </NavigationContainer>
  );
}

Patrón Stack: navegación jerárquica

El stack es ideal para flujos donde “apilas” pantallas (lista → detalle → edición). Con native-stack obtienes transiciones nativas y mejor rendimiento.

Crear un Stack Navigator

import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

function RootStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  );
}

Enviar y leer parámetros de ruta

Envía parámetros con navigation.navigate:

function HomeScreen({ navigation }) {
  return (
    <Button
      title="Ver producto"
      onPress={() => navigation.navigate('Details', { productId: 'p_123' })}
    />
  );
}

Lee parámetros desde route.params:

function DetailsScreen({ route }) {
  const { productId } = route.params;
  return <Text>Producto: {productId}</Text>;
}

Tip profesional: tipado de rutas (TypeScript)

En apps grandes, tipar rutas evita errores y hace autocompletado real.

type RootStackParamList = {
  Home: undefined;
  Details: { productId: string };
};

Patrón Tabs: secciones principales

Las tabs son ideales para secciones de primer nivel (Feed, Buscar, Perfil). Normalmente cada tab contiene su propio stack interno.

Tabs con stacks anidados

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Tab = createBottomTabNavigator();
const HomeStack = createNativeStackNavigator();
const ProfileStack = createNativeStackNavigator();

function HomeStackNavigator() {
  return (
    <HomeStack.Navigator>
      <HomeStack.Screen name="Home" component={HomeScreen} />
      <HomeStack.Screen name="Details" component={DetailsScreen} />
    </HomeStack.Navigator>
  );
}

function ProfileStackNavigator() {
  return (
    <ProfileStack.Navigator>
      <ProfileStack.Screen name="Profile" component={ProfileScreen} />
      <ProfileStack.Screen name="Settings" component={SettingsScreen} />
    </ProfileStack.Navigator>
  );
}

function AppTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="HomeTab" component={HomeStackNavigator} options={{ title: 'Inicio' }} />
      <Tab.Screen name="ProfileTab" component={ProfileStackNavigator} options={{ title: 'Perfil' }} />
    </Tab.Navigator>
  );
}

Este patrón evita stacks gigantes y permite que cada sección evolucione de forma independiente.

Patrón Drawer: navegación lateral

El drawer es útil para apps con muchas secciones o navegación secundaria (panel administrativo, categorías, herramientas). A menudo envuelve tabs o stacks.

import { createDrawerNavigator } from '@react-navigation/drawer';

const Drawer = createDrawerNavigator();

function AppDrawer() {
  return (
    <Drawer.Navigator>
      <Drawer.Screen name="Main" component={AppTabs} options={{ title: 'Principal' }} />
      <Drawer.Screen name="Help" component={HelpScreen} options={{ title: 'Ayuda' }} />
    </Drawer.Navigator>
  );
}

Control del encabezado (header) sin hacks

El header se controla con options a nivel de pantalla o navigator. Puedes definir valores estáticos o dinámicos.

Opciones estáticas

<Stack.Screen
  name="Details"
  component={DetailsScreen}
  options={{ title: 'Detalle', headerBackTitleVisible: false }}
/>

Opciones dinámicas según params

<Stack.Screen
  name="Details"
  component={DetailsScreen}
  options={({ route }) => ({
    title: `Producto ${route.params.productId}`,
  })}
/>

Modificar el header desde la pantalla

Útil cuando el título depende de datos cargados. Usa useLayoutEffect para evitar “parpadeos”.

import { useLayoutEffect } from 'react';

function DetailsScreen({ navigation, route }) {
  useLayoutEffect(() => {
    navigation.setOptions({ title: `Producto ${route.params.productId}` });
  }, [navigation, route.params.productId]);

  return null;
}

Header personalizado

Si necesitas un header propio (branding, buscador, etc.), puedes reemplazarlo:

<Stack.Screen
  name="Home"
  component={HomeScreen}
  options={{
    header: () => <MyCustomHeader />,
  }}
/>

Deep linking básico

El deep linking permite abrir pantallas desde URLs (por ejemplo, desde un email o notificación). React Navigation lo gestiona con una configuración de linking en NavigationContainer.

Configurar linking

import { NavigationContainer } from '@react-navigation/native';

const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  config: {
    screens: {
      HomeTab: {
        screens: {
          Home: 'home',
          Details: 'product/:productId',
        },
      },
      ProfileTab: {
        screens: {
          Profile: 'me',
          Settings: 'settings',
        },
      },
    },
  },
};

export default function App() {
  return (
    <NavigationContainer linking={linking}>
      <AppDrawer />
    </NavigationContainer>
  );
}

Con esto, myapp://product/p_123 o https://myapp.com/product/p_123 puede abrir Details con productId.

Validación de parámetros en deep links

En apps grandes, trata los params como entrada externa: valida y maneja estados vacíos.

function DetailsScreen({ route }) {
  const productId = route.params?.productId;
  if (!productId) return <Text>Producto no válido</Text>;
  return <Text>Producto: {productId}</Text>;
}

Organización por feature para escalar la navegación

Cuando la app crece, el error típico es tener un único archivo con todos los navegadores y pantallas. Una estructura por feature mantiene el código modular y reduce conflictos en equipo.

Estructura recomendada

src/
  app/
    navigation/
      RootNavigator.tsx
      linking.ts
      types.ts
  features/
    auth/
      navigation/
        AuthNavigator.tsx
      screens/
        LoginScreen.tsx
        RegisterScreen.tsx
      hooks/
        useAuth.ts
    onboarding/
      navigation/
        OnboardingNavigator.tsx
      screens/
        WelcomeScreen.tsx
        PermissionsScreen.tsx
    home/
      navigation/
        HomeNavigator.tsx
      screens/
        HomeScreen.tsx
        DetailsScreen.tsx

Regla práctica

  • Cada feature expone su propio navigator (Stack o el que aplique).
  • RootNavigator compone features (Auth, Onboarding, App).
  • Los tipos de rutas se centralizan o se componen por feature para evitar duplicación.

Composición en RootNavigator

import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { AuthNavigator } from '@/features/auth/navigation/AuthNavigator';
import { OnboardingNavigator } from '@/features/onboarding/navigation/OnboardingNavigator';
import { AppDrawer } from '@/app/navigation/AppDrawer';

const Root = createNativeStackNavigator();

export function RootNavigator({ isSignedIn, needsOnboarding }) {
  return (
    <Root.Navigator screenOptions={{ headerShown: false }}>
      {!isSignedIn ? (
        <Root.Screen name="Auth" component={AuthNavigator} />
      ) : needsOnboarding ? (
        <Root.Screen name="Onboarding" component={OnboardingNavigator} />
      ) : (
        <Root.Screen name="App" component={AppDrawer} />
      )}
    </Root.Navigator>
  );
}

Este patrón evita duplicar pantallas en múltiples stacks y mantiene un único punto de decisión.

Manejo de estado de navegación: focus y ciclo de vida de pantallas

En navegación, una pantalla puede estar montada pero no visible (por ejemplo, en tabs). Por eso es clave reaccionar al focus y a eventos del navigator.

Detectar si una pantalla está enfocada

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

function ProfileScreen() {
  const isFocused = useIsFocused();
  return <Text>{isFocused ? 'En foco' : 'En background'}</Text>;
}

Ejecutar efectos al entrar en foco (sin duplicar lógica)

Ideal para refrescar datos cuando el usuario vuelve a la pantalla.

import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';

function HomeScreen() {
  useFocusEffect(
    useCallback(() => {
      // Se ejecuta al entrar en foco
      // Ej: refetch()
      return () => {
        // Se ejecuta al perder el foco
      };
    }, [])
  );

  return null;
}

Escuchar eventos del navigator

Útil para analítica o para cancelar tareas al navegar.

function DetailsScreen({ navigation }) {
  useEffect(() => {
    const unsub = navigation.addListener('beforeRemove', (e) => {
      // e.g. confirmar salida si hay cambios sin guardar
      // e.preventDefault();
    });
    return unsub;
  }, [navigation]);

  return null;
}

Evitar “fetch” duplicado en tabs

  • Si la pantalla permanece montada, usa useFocusEffect para refrescar solo al volver.
  • Si quieres desmontar al salir (menos común), usa unmountOnBlur: true en la screen/tab, sabiendo que perderás estado local.
<Tab.Screen
  name="HomeTab"
  component={HomeStackNavigator}
  options={{ unmountOnBlur: false }}
/>

Flujos típicos sin duplicar lógica: autenticación y onboarding

Un error común es duplicar pantallas (por ejemplo, tener Home en dos stacks distintos) o duplicar reglas (validar sesión en cada pantalla). La solución es centralizar la decisión de flujo en un Root Navigator y mantener estados globales (auth/onboarding) fuera de la navegación.

Modelo de estados para decidir el flujo

EstadoQué muestra
No autenticadoAuthNavigator (Login/Register)
Autenticado + onboarding pendienteOnboardingNavigator
Autenticado + onboarding completoApp (Drawer/Tabs/Stacks)

AuthNavigator (stack simple)

import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Auth = createNativeStackNavigator();

export function AuthNavigator() {
  return (
    <Auth.Navigator>
      <Auth.Screen name="Login" component={LoginScreen} options={{ title: 'Acceder' }} />
      <Auth.Screen name="Register" component={RegisterScreen} options={{ title: 'Crear cuenta' }} />
    </Auth.Navigator>
  );
}

OnboardingNavigator (stack con control de back)

En onboarding suele interesar controlar el botón atrás o el gesto para evitar saltos inconsistentes.

const Onb = createNativeStackNavigator();

export function OnboardingNavigator() {
  return (
    <Onb.Navigator>
      <Onb.Screen
        name="Welcome"
        component={WelcomeScreen}
        options={{ headerBackVisible: false, gestureEnabled: false }}
      />
      <Onb.Screen name="Permissions" component={PermissionsScreen} options={{ title: 'Permisos' }} />
    </Onb.Navigator>
  );
}

Transiciones entre flujos sin “reset” manual innecesario

Si el RootNavigator depende de isSignedIn y needsOnboarding, al cambiar esos flags se re-renderiza el árbol y el usuario cae en el flujo correcto. Esto evita llamar reset desde múltiples pantallas.

Ejemplo: al completar onboarding, actualiza el estado global needsOnboarding = false y el root mostrará App.

Cuándo usar navigation.reset

Úsalo solo si necesitas limpiar historial dentro del mismo flujo (por ejemplo, después de un “checkout” para volver a Home sin permitir back a pago).

navigation.reset({
  index: 0,
  routes: [{ name: 'Home' }],
});

Buenas prácticas para una navegación mantenible

  • Nombres consistentes: usa sufijos como HomeTab, Root, Auth para evitar colisiones.
  • Params mínimos: pasa IDs, no objetos grandes; carga datos en la pantalla destino.
  • Headers por defecto: define screenOptions a nivel de navigator y sobrescribe por pantalla.
  • Separación por feature: cada feature controla sus pantallas y su navigator; el root solo compone.
  • Focus-aware: refresca datos con useFocusEffect en lugar de depender solo de useEffect.
  • Deep links: valida params y define rutas estables; evita cambiar paths sin estrategia de compatibilidad.

Ahora responde el ejercicio sobre el contenido:

En una app con tabs donde las pantallas pueden permanecer montadas aunque no estén visibles, ¿qué enfoque es más adecuado para refrescar datos cuando el usuario vuelve a una pantalla sin duplicar lógica?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

En tabs una pantalla puede quedar montada aunque no esté visible. useFocusEffect permite ejecutar efectos al ganar foco (por ejemplo, refrescar datos) y evitar lógica duplicada o recargas innecesarias.

Siguiente capítulo

Gestión de estado en React Native con hooks y arquitectura

Arrow Right Icon
Portada de libro electrónico gratuitaReact Native desde Cero a App Profesional
25%

React Native desde Cero a App Profesional

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.