React Native course Β· Module 3: Navigation

Practical Project: Atlantis Navigation

5 min read Β·
In this lesson7

Time to combine all the navigation techniques you have learned into a complete project!

Project Structure with Expo Router

app/
β”œβ”€β”€ _layout.tsx              # Root layout
β”œβ”€β”€ index.tsx                # Splash/Landing screen
β”œβ”€β”€ (auth)/
β”‚   β”œβ”€β”€ _layout.tsx          # Auth layout
β”‚   β”œβ”€β”€ login.tsx
β”‚   └── register.tsx
β”œβ”€β”€ (main)/
β”‚   β”œβ”€β”€ _layout.tsx          # Drawer layout
β”‚   └── (tabs)/
β”‚       β”œβ”€β”€ _layout.tsx      # Tab layout
β”‚       β”œβ”€β”€ home/
β”‚       β”‚   β”œβ”€β”€ _layout.tsx  # Stack for home
β”‚       β”‚   β”œβ”€β”€ index.tsx    # Feed
β”‚       β”‚   └── [postId].tsx # Post details
β”‚       β”œβ”€β”€ explore.tsx
β”‚       └── profile.tsx
β”œβ”€β”€ settings/
β”‚   β”œβ”€β”€ _layout.tsx
β”‚   β”œβ”€β”€ index.tsx
β”‚   β”œβ”€β”€ account.tsx
β”‚   └── notifications.tsx
└── +not-found.tsx

Root Layout with AuthContext

// app/_layout.tsx
import { Stack } from 'expo-router';
import { AuthProvider, useAuth } from '@/providers/AuthProvider';

function RootLayoutNav() {
  const { user, isLoading } = useAuth();

  if (isLoading) {
    return <LoadingScreen />;
  }

  return (
    <Stack screenOptions={{ headerShown: false }}>
      {user ? (
        <Stack.Screen name="(main)" />
      ) : (
        <Stack.Screen name="(auth)" />
      )}
      <Stack.Screen name="+not-found" />
    </Stack>
  );
}

export default function RootLayout() {
  return (
    <AuthProvider>
      <RootLayoutNav />
    </AuthProvider>
  );
}

Auth Provider

// providers/AuthProvider.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { router } from 'expo-router';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface User {
  id: string;
  email: string;
  name: string;
}

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  register: (email: string, password: string, name: string) => Promise<void>;
  logout: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    loadUser();
  }, []);

  const loadUser = async () => {
    try {
      const userData = await AsyncStorage.getItem('user');
      if (userData) {
        setUser(JSON.parse(userData));
      }
    } finally {
      setIsLoading(false);
    }
  };

  const login = async (email: string, password: string) => {
    // Simulated API call
    const mockUser = { id: '1', email, name: 'Citizen of Atlantis' };
    await AsyncStorage.setItem('user', JSON.stringify(mockUser));
    setUser(mockUser);
    router.replace('/(main)/(tabs)/home');
  };

  const register = async (email: string, password: string, name: string) => {
    const mockUser = { id: '1', email, name };
    await AsyncStorage.setItem('user', JSON.stringify(mockUser));
    setUser(mockUser);
    router.replace('/(main)/(tabs)/home');
  };

  const logout = async () => {
    await AsyncStorage.removeItem('user');
    setUser(null);
    router.replace('/(auth)/login');
  };

  return (
    <AuthContext.Provider value={{ user, isLoading, login, register, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
};

Main Layout with Drawer

// app/(main)/_layout.tsx
import { Drawer } from 'expo-router/drawer';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { CustomDrawerContent } from '@/components/CustomDrawerContent';

export default function MainLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Drawer
        drawerContent={(props) => <CustomDrawerContent {...props} />}
        screenOptions={{
          drawerStyle: { backgroundColor: '#0a2e4a', width: 280 },
          drawerActiveTintColor: '#00d4ff',
          drawerInactiveTintColor: '#a0aec0',
        }}
      >
        <Drawer.Screen
          name="(tabs)"
          options={{ title: 'Home', headerShown: false }}
        />
        <Drawer.Screen
          name="../settings"
          options={{ title: 'Settings' }}
        />
      </Drawer>
    </GestureHandlerRootView>
  );
}

Tab Layout

// app/(main)/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuth } from '@/providers/AuthProvider';

export default function TabLayout() {
  const { user } = useAuth();

  return (
    <Tabs
      screenOptions={{
        tabBarStyle: {
          backgroundColor: '#0a2e4a',
          borderTopColor: '#1a5276',
        },
        tabBarActiveTintColor: '#00d4ff',
        tabBarInactiveTintColor: '#6c7a89',
        headerStyle: { backgroundColor: '#0a2e4a' },
        headerTintColor: '#fff',
      }}
    >
      <Tabs.Screen
        name="home"
        options={{
          title: 'Home',
          headerShown: false, // Stack has its own header
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="explore"
        options={{
          title: 'Explore',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="compass" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: user?.name || 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Home Stack

// app/(main)/(tabs)/home/_layout.tsx
import { Stack } from 'expo-router';

export default function HomeLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: '#0a2e4a' },
        headerTintColor: '#fff',
      }}
    >
      <Stack.Screen
        name="index"
        options={{ title: 'News' }}
      />
      <Stack.Screen
        name="[postId]"
        options={{ title: 'Post' }}
      />
    </Stack>
  );
}

Home Feed

// app/(main)/(tabs)/home/index.tsx
import { FlatList, Pressable, View, Text, StyleSheet } from 'react-native';
import { Link } from 'expo-router';

const posts = [
  { id: '1', title: 'New discovery in the depths', preview: 'An ancient artifact was found...' },
  { id: '2', title: 'Festival of Poseidon', preview: 'The great celebration is just a week away...' },
  { id: '3', title: 'Sea map update', preview: 'New routes have been added...' },
];

export default function HomeFeed() {
  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.id}
      contentContainerStyle={styles.container}
      renderItem={({ item }) => (
        <Link href={`/home/${item.id}`} asChild>
          <Pressable style={styles.postCard}>
            <Text style={styles.postTitle}>{item.title}</Text>
            <Text style={styles.postPreview}>{item.preview}</Text>
          </Pressable>
        </Link>
      )}
    />
  );
}

This project combines all types of navigation: Stack, Tabs, Drawer, and Deep Linking with Expo Router. It is a solid foundation for any larger mobile application!

Code for this lesson: App.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createDrawerNavigator } from '@react-navigation/drawer';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Drawer = createDrawerNavigator();
const Tab = createBottomTabNavigator();
const Stack = createNativeStackNavigator();

function Screen({ label }) {
  return (
    <View style={styles.screen}>
      <Text style={styles.title}>{label}</Text>
    </View>
  );
}

// Stack nested inside the "Feed" tab
function FeedStack() {
  return (
    <Stack.Navigator>
      {/* TODO: add a <Stack.Screen name="FeedList"> rendering <Screen label="List" /> */}
      {/* TODO: add a <Stack.Screen name="FeedDetails"> rendering <Screen label="Details" /> */}

    </Stack.Navigator>
  );
}

// Tabs containing the stack and a settings screen
function MainTabs() {
  return (
    <Tab.Navigator>
      {/* TODO: add a "Feed" tab with component={FeedStack} */}
      {/* TODO: add a "Settings" tab rendering <Screen label="Settings" /> */}

    </Tab.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      {/* TODO: build a <Drawer.Navigator> where:
          - the "Home" screen uses component={MainTabs}
          - the "About" screen renders <Screen label="About Atlantis" /> */}
      <Drawer.Navigator>

      </Drawer.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#1a5276',
  },
  title: {
    fontSize: 22,
    color: '#fff',
    fontWeight: '600',
  },
});

Docs entries used in this lesson

Spotted a mistake in this lesson?

Hands-on tasks in the game

  • Code editor

    Complete the application navigation code.

Useful articles