Navigation Concepts: Screens, Routes, and Common App Flows

Capítulo 8

Estimated reading time: 11 minutes

+ Exercise

Why Navigation Matters in React Native Apps

Most real apps are not a single screen. Users move between a home screen, details screens, settings, authentication, and sometimes modal flows like “create” or “filter.” Navigation is the system that defines how your app moves between these screens, how it remembers where the user has been, and how it presents transitions (push, slide, modal) in a way that matches platform expectations.

In React Native, navigation is usually handled by a dedicated library (commonly React Navigation). The key idea is that navigation is state: the app has a “current route” (and often a stack of previous routes), and navigation actions update that state. Your UI renders based on the current navigation state.

Core Concepts: Screens, Routes, and Navigators

Screen

A screen is a React component that represents a full page of the app. It typically occupies the entire viewport and is registered with a navigator. Examples: HomeScreen, ProductDetailsScreen, SettingsScreen.

Route

A route is an entry in the navigation state that points to a screen and includes identifying information. A route usually has:

  • name: a string key used to navigate (for example, "Home" or "ProductDetails").
  • params: optional data passed to the screen (for example, { productId: "p42" }).

Think of a route as “a specific instance of a screen,” possibly with parameters.

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

Download the app

Navigator

A navigator is a component that manages a set of screens and the transitions between them. Different navigators model different patterns:

  • Stack navigator: push/pop like a call stack; common for drill-down flows (list → details → more details).
  • Tab navigator: switch between top-level sections; each tab can have its own stack.
  • Drawer navigator: side menu for top-level destinations; often used in admin or content-heavy apps.
  • Modal presentation: a screen presented on top of others, often for creation flows or quick tasks.

Mental Model: Navigation State and Actions

Navigation can be understood as two parts:

  • State: which routes exist and which one is active (and in what order, for stacks).
  • Actions: operations that change the state, such as navigate, push, goBack, replace, and reset.

When you call navigation.navigate("ProductDetails", { productId }), you are dispatching an action that updates the navigation state. The navigator then renders the new active screen and animates the transition.

Practical Setup: A Basic Stack with Two Screens

The following example shows a minimal stack navigator with a Home screen and a Details screen. This is a common starting point for understanding screens and routes.

Step 1: Install navigation dependencies

In a typical React Native project, you install React Navigation and the stack navigator. Commands vary depending on whether you use Expo or the React Native CLI, but conceptually you need:

  • @react-navigation/native
  • @react-navigation/native-stack (or @react-navigation/stack)
  • native dependencies for gestures and screens (often react-native-screens, react-native-safe-area-context, and react-native-gesture-handler)

Follow the official install steps for your environment to ensure native modules are configured correctly.

Step 2: Create screens

import React from "react";import { View, Text, Button } from "react-native";export function HomeScreen({ navigation }) {  return (    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>      <Text>Home</Text>      <Button        title="Go to details"        onPress={() => navigation.navigate("Details", { itemId: 42 })}      />    </View>  );}export function DetailsScreen({ route, navigation }) {  const { itemId } = route.params ?? {};  return (    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>      <Text>Details</Text>      <Text>Item ID: {String(itemId)}</Text>      <Button title="Go back" onPress={() => navigation.goBack()} />    </View>  );}

Key points:

  • Each screen receives a navigation prop (to perform actions) and a route prop (to read route name and params).
  • navigate selects a route by name and optionally passes params.
  • goBack pops the current screen off the stack.

Step 3: Register screens in a stack navigator

import React from "react";import { NavigationContainer } from "@react-navigation/native";import { createNativeStackNavigator } from "@react-navigation/native-stack";import { HomeScreen, DetailsScreen } from "./screens";const Stack = createNativeStackNavigator();export default function App() {  return (    <NavigationContainer>      <Stack.Navigator initialRouteName="Home">        <Stack.Screen name="Home" component={HomeScreen} />        <Stack.Screen name="Details" component={DetailsScreen} />      </Stack.Navigator>    </NavigationContainer>  );}

NavigationContainer holds the navigation state for the whole app. The stack navigator defines which routes exist and how they transition.

Understanding Common Navigation Actions

navigate vs push

In a stack navigator:

  • navigate("Details", params) goes to a route by name. If that route is already on top, it may do nothing; if it exists in the stack, behavior can vary depending on configuration.
  • push("Details", params) always adds a new instance to the top of the stack, even if you are already on Details.
// From Details, open another Details instancenavigation.push("Details", { itemId: 99 });

replace

replace swaps the current route with a new one. This is useful when you do not want the user to go back to the previous screen (for example, after completing onboarding or after a successful login).

navigation.replace("Home");

reset

reset replaces the entire navigation state. This is commonly used when switching between major flows, such as “Auth flow” to “App flow.”

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

Passing Data Between Screens (Route Params)

Route params are the standard way to pass small pieces of data when navigating. Typical examples include IDs, filter settings, or a mode flag (view vs edit).

Step-by-step: Navigate with params and read them

  • From a list screen, navigate to details with an ID.
  • In the details screen, read route.params and fetch or select the correct data.
// List screenonPress={() => navigation.navigate("ProductDetails", { productId: "p42" })}// Details screenconst { productId } = route.params; // "p42"

Practical guidance:

  • Prefer passing stable identifiers (like productId) rather than large objects, especially if the object can change or is not serializable.
  • Keep params minimal; treat them like URL parameters.

Updating params

Sometimes you want to update the current route’s params (for example, after the user changes a filter). You can set params on the current route.

navigation.setParams({ sort: "price_desc" });

Header, Titles, and Screen Options

Stack navigators often provide a header (top bar) with a title and back button. You can configure it per screen or globally.

<Stack.Navigator  screenOptions={{ headerShown: true }}>  <Stack.Screen    name="Home"    component={HomeScreen}    options={{ title: "Welcome" }}  />  <Stack.Screen    name="Details"    component={DetailsScreen}    options={({ route }) => ({      title: `Item ${route.params?.itemId ?? ""}`,    })}  /></Stack.Navigator>

Use dynamic options when the header depends on route params.

Common App Flows and How to Model Them

1) Authentication Flow (Logged Out vs Logged In)

A very common pattern is to show different navigation trees depending on whether the user is authenticated. Conceptually, you have two separate navigators:

  • Auth stack: Sign In, Sign Up, Forgot Password
  • Main app: Tabs/Drawer + stacks for content

The key is that you do not “navigate” from Auth to App like a normal screen transition; instead, you render a different navigator based on auth state. This avoids deep back stacks that let users return to Sign In after logging in.

function AuthStack() {  return (    <Stack.Navigator>      <Stack.Screen name="SignIn" component={SignInScreen} />      <Stack.Screen name="SignUp" component={SignUpScreen} />    </Stack.Navigator>  );}function AppStack() {  return (    <Stack.Navigator>      <Stack.Screen name="Home" component={HomeScreen} />    </Stack.Navigator>  );}export default function App() {  const isSignedIn = false; // replace with real auth state  return (    <NavigationContainer>      {isSignedIn ? <AppStack /> : <AuthStack />}    </NavigationContainer>  );}

Step-by-step: Prevent going back to Sign In after login

  • When login succeeds, update your auth state (for example, store a token and set isSignedIn to true).
  • The root component re-renders and swaps AuthStack for AppStack.
  • Because the navigator tree changed, the old routes are gone; back navigation won’t return to Sign In.

2) Tab-Based Main App with Nested Stacks

Many apps have tabs like “Feed,” “Search,” and “Profile.” Each tab usually has its own stack so users can drill down without losing their place in other tabs.

Conceptually:

  • Bottom tabs at the top level
  • Inside each tab, a stack navigator
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";const Tab = createBottomTabNavigator();function FeedStack() {  return (    <Stack.Navigator>      <Stack.Screen name="FeedHome" component={FeedHomeScreen} />      <Stack.Screen name="Post" component={PostScreen} />    </Stack.Navigator>  );}function ProfileStack() {  return (    <Stack.Navigator>      <Stack.Screen name="ProfileHome" component={ProfileHomeScreen} />      <Stack.Screen name="EditProfile" component={EditProfileScreen} />    </Stack.Navigator>  );}function MainTabs() {  return (    <Tab.Navigator>      <Tab.Screen name="Feed" component={FeedStack} />      <Tab.Screen name="Profile" component={ProfileStack} />    </Tab.Navigator>  );}

Practical guidance:

  • Keep tab route names distinct from inner stack route names to avoid confusion in debugging.
  • When navigating to a nested screen, you may need to specify both the tab and the inner screen depending on your structure.

3) Modal Flows (Create, Compose, Pickers)

Modals are useful when a task is temporary and should not feel like a deep navigation step. Examples: “Create Post,” “Select Location,” “Add Payment Method.”

A common approach is to use a root stack with a modal presentation group:

const RootStack = createNativeStackNavigator();function RootNavigator() {  return (    <RootStack.Navigator>      <RootStack.Screen        name="Main"        component={MainTabs}        options={{ headerShown: false }}      />      <RootStack.Screen        name="Compose"        component={ComposeScreen}        options={{ presentation: "modal", title: "New Post" }}      />    </RootStack.Navigator>  );}

Step-by-step: Open and close a modal

  • From any screen inside MainTabs, call navigation.navigate("Compose") (if the route is available in the parent navigator).
  • In the modal screen, call navigation.goBack() to dismiss.

4) Onboarding Flow (First Run Experience)

Onboarding is often a multi-screen sequence shown only once. The key requirement is to prevent users from returning to onboarding after finishing it.

Two common strategies:

  • Conditional navigator: show onboarding stack only if hasCompletedOnboarding is false.
  • Reset navigation: after completion, reset to the main route.
// After onboarding completionnavigation.reset({  index: 0,  routes: [{ name: "Main" }],});

Deep Linking and Route-Like Thinking

Even if you do not implement deep linking immediately, it helps to think of routes like URLs: a route name plus params describes a location in the app. This mindset encourages:

  • Passing IDs rather than entire objects
  • Keeping screens resilient when params are missing (show an error state or fallback)
  • Designing navigation paths that map cleanly to user intents (open product by ID, open order by ID)

When you later add deep linking, you can map URLs to route names and params more naturally.

Practical Patterns for Real Apps

Pattern: “List → Details → Edit” with correct back behavior

Suppose you have a list of items, a details screen, and an edit screen. A typical flow is:

  • List navigates to Details with itemId
  • Details navigates to Edit with the same itemId
  • After saving, return to Details (not to List)
// Details screen: open editnavigation.navigate("EditItem", { itemId });// Edit screen: after save, go backnavigation.goBack();

If you want to prevent returning to Edit after saving (for example, you navigate to a “Success” screen), use replace or reset depending on the desired history.

Pattern: “Search” with a results screen and persistent query

Search flows often need to preserve the query when users move between results and details. You can:

  • Store the query in route params for the results route
  • Or keep it in a shared state/store if multiple screens need it
// Navigate to results with a querynavigation.navigate("SearchResults", { q: "headphones" });// Results screen reads qconst q = route.params?.q ?? "";

Pattern: Guarding routes (require login)

Some screens should only be accessible when authenticated. The cleanest approach is to place protected screens only inside the authenticated navigator tree. If you must handle it inside a screen (for example, a deep link opens a protected route), treat it as a redirect:

  • If not signed in, navigate to Sign In and remember the intended destination (route name + params).
  • After sign in, navigate to the intended destination and clear the pending redirect.
// Pseudocode for redirect intentconst intended = { name: "Checkout", params: { cartId: "c1" } };navigation.navigate("SignIn", { intended });

Common Mistakes and How to Avoid Them

Using navigation for data management

Navigation params are not a replacement for app state. Use params for “where to go” and “what to open,” not as the primary store for complex mutable data. Prefer passing IDs and loading the latest data in the destination screen.

Not handling missing params

Screens can be opened in unexpected ways (back navigation, deep links, restored state). Defensive code helps:

const itemId = route.params?.itemId;if (!itemId) {  return <Text>Missing itemId</Text>;}

Overly deep nesting without a plan

Nesting navigators is normal, but it should reflect app structure. A practical approach is:

  • Root: decides between Auth / Onboarding / Main
  • Main: tabs or drawer for top-level sections
  • Each section: stack for drill-down
  • Root modal routes: compose, pickers, global modals

Confusing route names across navigators

When multiple stacks contain a "Details" route, debugging becomes harder. Prefer namespaced route names (for example, "FeedPost", "ShopProduct") or keep them unique at least within the same navigator level.

Now answer the exercise about the content:

In a stack navigator, which action should you use when you want to open a new instance of the same screen even if you are already on it?

You are right! Congratulations, now go to the next page

You missed! Try again.

In a stack navigator, push always adds a new instance of a route to the top of the stack, even if you are already on that same screen. navigate may reuse an existing route, and replace swaps the current route.

Next chapter

Implementing Stack and Tab Navigation Patterns

Arrow Right Icon
Free Ebook cover React Native Basics: Components, Styling, and Navigation Concepts
67%

React Native Basics: Components, Styling, and Navigation Concepts

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.