Cours React Native · Module 3: Navigation
Projet pratique : Navigation Atlantis
Dans cette leçon7
Il est temps de combiner toutes les techniques de navigation que tu as apprises dans un projet complet !
Structure du projet avec Expo Router
app/
├── _layout.tsx # Layout racine
├── index.tsx # Écran Splash/Landing
├── (auth)/
│ ├── _layout.tsx # Layout Auth
│ ├── login.tsx
│ └── register.tsx
├── (main)/
│ ├── _layout.tsx # Layout Drawer
│ └── (tabs)/
│ ├── _layout.tsx # Layout Tab
│ ├── home/
│ │ ├── _layout.tsx # Stack pour home
│ │ ├── index.tsx # Feed
│ │ └── [postId].tsx # Détails d'un post
│ ├── explore.tsx
│ └── profile.tsx
├── settings/
│ ├── _layout.tsx
│ ├── index.tsx
│ ├── account.tsx
│ └── notifications.tsx
└── +not-found.tsxLayout racine avec 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) => {
// Appel API simulé
const mockUser = { id: '1', email, name: 'Citoyen d\'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;
};Layout principal avec 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: 'Accueil', headerShown: false }}
/>
<Drawer.Screen
name="../settings"
options={{ title: 'Réglages' }}
/>
</Drawer>
</GestureHandlerRootView>
);
}Layout des onglets
// 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: 'Accueil',
headerShown: false, // Le Stack a son propre en-tête
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explorer',
tabBarIcon: ({ color, size }) => (
<Ionicons name="compass" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: user?.name || 'Profil',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}Stack de la page d'accueil
// 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: 'Actualités' }}
/>
<Stack.Screen
name="[postId]"
options={{ title: 'Post' }}
/>
</Stack>
);
}Feed de la page d'accueil
// 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: 'Nouvelle découverte dans les profondeurs', preview: 'Un ancien artefact a été trouvé...' },
{ id: '2', title: 'Festival de Poséidon', preview: 'La grande célébration n\'est plus qu\'à une semaine...' },
{ id: '3', title: 'Mise à jour de la carte marine', preview: 'De nouvelles routes ont été ajoutées...' },
];
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>
)}
/>
);
}Ce projet combine tous les types de navigation : Stack, Tabs, Drawer et Deep Linking avec Expo Router. C'est une base solide pour n'importe quelle application mobile de plus grande envergure !
Code de cette leçon : 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 imbriqué dans l'onglet Feed
function FeedStack() {
return (
<Stack.Navigator>
{/* TODO : ajoute un <Stack.Screen name="FeedList"> qui affiche <Screen label="Liste" /> */}
{/* TODO : ajoute un <Stack.Screen name="FeedDetails"> qui affiche <Screen label="Détails" /> */}
</Stack.Navigator>
);
}
// Onglets contenant le stack et un écran de paramètres
function MainTabs() {
return (
<Tab.Navigator>
{/* TODO : ajoute un onglet "Feed" avec component={FeedStack} */}
{/* TODO : ajoute un onglet "Settings" qui affiche <Screen label="Paramètres" /> */}
</Tab.Navigator>
);
}
export default function App() {
return (
<NavigationContainer>
{/* TODO : construis un <Drawer.Navigator> où :
- l'écran "Home" utilise component={MainTabs}
- l'écran "About" affiche <Screen label="À propos de l'Atlantide" /> */}
<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',
},
});Entrées de la documentation utilisées dans cette leçon
Tu as repéré une erreur dans cette leçon ?
Tâches pratiques dans le jeu
- Éditeur de code
Complète le code de navigation de l'application.