Lesson 17

Capstone: Building a Complete App

Time to put everything together. We'll build a note-taking app with a complete navigation structure: authentication, a drawer for top-level navigation, tabs for main sections, stacks for drill-down, and a modal for creating new notes. This combines every concept from the course into one working app.

The Navigation Structure

Auth check
├── Login (when signed out)
└── Drawer (when signed in)
    ├── Main Tabs
    │   ├── Notes Stack
    │   │   ├── NotesList
    │   │   └── NoteDetail
    │   └── Profile
    └── About
Modal: CreateNote (slides up over everything)

Type Definitions

Start with the TypeScript param lists for every navigator:

type AuthStackParamList = {
  Login: undefined;
};

type NotesStackParamList = {
  NotesList: undefined;
  NoteDetail: { noteId: string; title: string };
};

type TabParamList = {
  NotesTab: undefined;
  Profile: undefined;
};

type DrawerParamList = {
  MainTabs: undefined;
  About: undefined;
};

type RootStackParamList = {
  Main: undefined;
  CreateNote: undefined;
};

Screen Components

The individual screens are intentionally simple — the focus is on the navigation wiring:

import React from 'react';
import { View, Text, Button, FlatList, TouchableOpacity, TextInput, StyleSheet } from 'react-native';

const AuthContext = React.createContext<{
  signIn: () => void;
  signOut: () => void;
} | null>(null);

function useAuth() {
  const ctx = React.useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be inside AuthProvider');
  return ctx;
}

const NOTES = [
  { id: '1', title: 'Shopping List', body: 'Milk, eggs, bread' },
  { id: '2', title: 'Meeting Notes', body: 'Discuss Q2 roadmap' },
  { id: '3', title: 'Ideas', body: 'Build a note-taking app' },
];

function LoginScreen() {
  const { signIn } = useAuth();
  return (
    <View style={styles.center}>
      <Text style={styles.heading}>Notes App</Text>
      <Text style={styles.subtitle}>Sign in to get started</Text>
      <Button title="Sign In" onPress={signIn} />
    </View>
  );
}

function NotesListScreen({ navigation }) {
  return (
    <View style={{ flex: 1 }}>
      <FlatList
        data={NOTES}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <TouchableOpacity
            style={styles.noteItem}
            onPress={() => navigation.navigate('NoteDetail', { noteId: item.id, title: item.title })}
          >
            <Text style={styles.noteTitle}>{item.title}</Text>
            <Text style={styles.noteBody} numberOfLines={1}>{item.body}</Text>
          </TouchableOpacity>
        )}
      />
      <View style={styles.fab}>
        <Button title="+ New Note" onPress={() => navigation.navigate('CreateNote')} />
      </View>
    </View>
  );
}

function NoteDetailScreen({ route }) {
  const note = NOTES.find((n) => n.id === route.params.noteId);
  return (
    <View style={styles.center}>
      <Text style={styles.heading}>{note?.title}</Text>
      <Text style={styles.body}>{note?.body}</Text>
    </View>
  );
}

function ProfileScreen() {
  const { signOut } = useAuth();
  return (
    <View style={styles.center}>
      <Text style={styles.heading}>Profile</Text>
      <Text style={styles.subtitle}>Jason Brown</Text>
      <Button title="Sign Out" onPress={signOut} />
    </View>
  );
}

function AboutScreen() {
  return (
    <View style={styles.center}>
      <Text style={styles.heading}>About</Text>
      <Text style={styles.body}>Notes App v1.0 — Built with React Navigation v7</Text>
    </View>
  );
}

function CreateNoteScreen({ navigation }) {
  const [title, setTitle] = React.useState('');
  return (
    <View style={styles.modal}>
      <TextInput
        style={styles.input}
        placeholder="Note title"
        value={title}
        onChangeText={setTitle}
        autoFocus
      />
      <Button
        title="Save"
        onPress={() => {
          console.log('Created note:', title);
          navigation.goBack();
        }}
      />
    </View>
  );
}

Wiring the Navigators

Now assemble them from the inside out — Notes Stack, then Tabs, then Drawer, then the Root Stack with auth:

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createDrawerNavigator, DrawerContentScrollView, DrawerItemList, DrawerItem } from '@react-navigation/drawer';
import { Ionicons } from '@expo/vector-icons';

const RootStack = createNativeStackNavigator();
const AuthStack = createNativeStackNavigator();
const NotesStack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
const Drawer = createDrawerNavigator();

function NotesStackNavigator() {
  return (
    <NotesStack.Navigator>
      <NotesStack.Screen name="NotesList" component={NotesListScreen} options={{ title: 'Notes' }} />
      <NotesStack.Screen
        name="NoteDetail"
        component={NoteDetailScreen}
        options={({ route }) => ({ title: route.params.title })}
      />
    </NotesStack.Navigator>
  );
}

function MainTabs() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        headerShown: false,
        tabBarIcon: ({ color, size }) => {
          const iconName = route.name === 'NotesTab' ? 'document-text-outline' : 'person-outline';
          return <Ionicons name={iconName} size={size} color={color} />;
        },
      })}
    >
      <Tab.Screen name="NotesTab" component={NotesStackNavigator} options={{ title: 'Notes' }} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  );
}

function CustomDrawerContent(props) {
  return (
    <DrawerContentScrollView {...props}>
      <View style={drawerStyles.header}>
        <Text style={drawerStyles.name}>Notes App</Text>
      </View>
      <DrawerItemList {...props} />
    </DrawerContentScrollView>
  );
}

function MainDrawer() {
  return (
    <Drawer.Navigator
      drawerContent={(props) => <CustomDrawerContent {...props} />}
      screenOptions={({ route }) => ({
        drawerIcon: ({ color, size }) => {
          const iconName = route.name === 'MainTabs' ? 'home-outline' : 'information-circle-outline';
          return <Ionicons name={iconName} size={size} color={color} />;
        },
      })}
    >
      <Drawer.Screen name="MainTabs" component={MainTabs} options={{ title: 'Home' }} />
      <Drawer.Screen name="About" component={AboutScreen} />
    </Drawer.Navigator>
  );
}

The Root — Auth + Modal

The top-level navigator handles authentication and the modal:

export default function App() {
  const [isSignedIn, setIsSignedIn] = React.useState(false);

  const authContext = React.useMemo(() => ({
    signIn: () => setIsSignedIn(true),
    signOut: () => setIsSignedIn(false),
  }), []);

  return (
    <AuthContext.Provider value={authContext}>
      <NavigationContainer>
        <RootStack.Navigator>
          {isSignedIn ? (
            <>
              <RootStack.Screen
                name="Main"
                component={MainDrawer}
                options={{ headerShown: false }}
              />
              <RootStack.Group screenOptions={{ presentation: 'modal' }}>
                <RootStack.Screen
                  name="CreateNote"
                  component={CreateNoteScreen}
                  options={({ navigation }) => ({
                    title: 'New Note',
                    headerLeft: () => (
                      <Button title="Cancel" onPress={() => navigation.goBack()} />
                    ),
                  })}
                />
              </RootStack.Group>
            </>
          ) : (
            <RootStack.Screen
              name="Login"
              component={LoginScreen}
              options={{ headerShown: false }}
            />
          )}
        </RootStack.Navigator>
      </NavigationContainer>
    </AuthContext.Provider>
  );
}

The Styles

const styles = StyleSheet.create({
  center: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 },
  heading: { fontSize: 28, fontWeight: 'bold', marginBottom: 8 },
  subtitle: { fontSize: 16, color: '#666', marginBottom: 24 },
  body: { fontSize: 16, color: '#333', textAlign: 'center' },
  noteItem: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' },
  noteTitle: { fontSize: 18, fontWeight: '600' },
  noteBody: { fontSize: 14, color: '#666', marginTop: 4 },
  fab: { padding: 16 },
  modal: { flex: 1, padding: 16, paddingTop: 24 },
  input: { fontSize: 18, borderBottomWidth: 1, borderBottomColor: '#ddd', paddingVertical: 8, marginBottom: 16 },
});

const drawerStyles = StyleSheet.create({
  header: { padding: 20, borderBottomWidth: 1, borderBottomColor: '#eee', marginBottom: 8 },
  name: { fontSize: 20, fontWeight: 'bold' },
});

What This Demonstrates

This single app uses every concept from the course:

  • Stack Navigator — Notes list to detail drill-down
  • Tab Navigator — Notes and Profile tabs with icons
  • Drawer Navigator — Home and About with custom drawer content
  • Nesting — Stack inside Tabs inside Drawer inside Root Stack
  • Authentication Flow — conditional Login vs Main screens
  • Modal — CreateNote presented as a modal with cancel button
  • Route Params — passing note data to detail screen
  • Dynamic Headers — title set from route params

Congratulations — you now have the knowledge to build any navigation structure a React Native app needs. The React Navigation docs are an excellent reference as you continue building.