Debutant 16 min de lecture · 3 445 mots

React Native pour débutants : Premier projet mobile complet 2025

Estimated reading time: 17 minutes

React Native permet de créer des applications mobiles natives pour iOS et Android en utilisant JavaScript et React. Dans ce guide complet, nous allons créer ensemble une application de gestion de tâches professionnelle, de l’installation à la publication.

1. Prérequis et installation de l’environnement

1.1 Configuration système requise

Pour développer sur iOS et Android :

  • macOS 12+ (pour iOS)
  • Windows 10+ ou Linux (pour Android uniquement)
  • Node.js 18+ et npm/yarn
  • Git
  • Outils de développement :

  • Visual Studio Code avec extensions
  • Xcode 14+ (macOS uniquement)
  • Android Studio
  • 1.2 Installation pas à pas

# 1. Vérifier Node.js (version 18+)
node --version
npm --version

# 2. Installer React Native CLI
npm install -g react-native-cli

# 3. Installer Expo CLI (optionnel, recommandé pour débuter)
npm install -g expo-cli

# 4. Vérifier l'installation
npx react-native --version

# 5. Installer Watchman (macOS, pour le hot reload)
brew install watchman

# 6. Installer CocoaPods (macOS, pour iOS)
sudo gem install cocoapods

1.3 Configuration Android Studio

# Télécharger Android Studio depuis developer.android.com

# Configurer les variables d'environnement (Linux/macOS)
# Ajouter à ~/.bashrc ou ~/.zshrc
export ANDROIDHOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROIDHOME/emulator
export PATH=$PATH:$ANDROIDHOME/platform-tools
export PATH=$PATH:$ANDROIDHOME/tools
export PATH=$PATH:$ANDROIDHOME/tools/bin

# Recharger la configuration
source ~/.zshrc

# Vérifier la configuration
echo $ANDROIDHOME

# Accepter les licences
yes | sdkmanager --licenses

1.4 Configuration Xcode (macOS)

# Installer Xcode depuis l'App Store

# Installer les Command Line Tools
xcode-select --install

# Accepter la licence
sudo xcodebuild -license accept

# Vérifier l'installation
xcodebuild -version

2. Création du projet : TodoApp

2.1 Initialisation avec TypeScript

# Créer le projet avec template TypeScript
npx react-native init TodoApp --template react-native-template-typescript

# Naviguer dans le projet
cd TodoApp

# Structure du projet
# TodoApp/
# ├── android/              # Projet Android natif
# ├── ios/                  # Projet iOS natif
# ├── src/                  # Code source (à créer)
# ├── App.tsx               # Composant racine
# ├── package.json          # Dépendances
# ├── tsconfig.json         # Configuration TypeScript
# └── metro.config.js       # Bundler Metro

# Créer la structure src/
mkdir -p src/{components,screens,navigation,utils,types,hooks,services}

2.2 Installation des dépendances

# Navigation
npm install @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs

# Dépendances navigation
npm install react-native-screens react-native-safe-area-context react-native-gesture-handler

# Stockage local
npm install @react-native-async-storage/async-storage

# Gestion d'état
npm install zustand

# Utilitaires
npm install date-fns react-native-uuid

# Types TypeScript
npm install -D @types/react @types/react-native

# iOS : Installation des pods
cd ios && pod install && cd ..

2.3 Configuration TypeScript stricte

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "lib": ["es2021"],
    "jsx": "react-native",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/": ["src/"],
      "@components/": ["src/components/"],
      "@screens/": ["src/screens/"],
      "@navigation/": ["src/navigation/"],
      "@utils/": ["src/utils/"],
      "@types/": ["src/types/"],
      "@hooks/": ["src/hooks/"],
      "@services/": ["src/services/"]
    }
  },
  "include": ["src//", "App.tsx"],
  "exclude": ["nodemodules", "android", "ios"]
}

3. Architecture et types TypeScript

3.1 Définition des types

// src/types/todo.ts
export interface Todo {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  dueDate: string | null;
  createdAt: string;
  updatedAt: string;
  category: string;
}

export interface TodoCategory {
  id: string;
  name: string;
  color: string;
  icon: string;
}

export type TodoFilter = 'all' | 'active' | 'completed';
export type TodoSort = 'date' | 'priority' | 'alphabetical';

// src/types/navigation.ts
import { StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
import { Todo } from './todo';

export type RootStackParamList = {
  TodoList: undefined;
  TodoDetail: { todoId: string };
  AddTodo: { category?: string };
  EditTodo: { todo: Todo };
  Settings: undefined;
};

export type TodoListNavigationProp = StackNavigationProp<
  RootStackParamList,
  'TodoList'
>;

export type TodoDetailNavigationProp = StackNavigationProp<
  RootStackParamList,
  'TodoDetail'
>;

export type TodoDetailRouteProp = RouteProp;

export type AddTodoNavigationProp = StackNavigationProp<
  RootStackParamList,
  'AddTodo'
>;

export type EditTodoNavigationProp = StackNavigationProp<
  RootStackParamList,
  'EditTodo'
>;

export type EditTodoRouteProp = RouteProp;

3.2 Service de stockage avec AsyncStorage

// src/services/storage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Todo, TodoCategory } from '@/types/todo';

const TODOSKEY = '@todos';
const CATEGORIESKEY = '@categories';

class StorageService {
  // Gestion des todos
  async getTodos(): Promise {
    try {
      const todosJson = await AsyncStorage.getItem(TODOSKEY);
      return todosJson ? JSON.parse(todosJson) : [];
    } catch (error) {
      console.error('Erreur lecture todos:', error);
      return [];
    }
  }

  async saveTodos(todos: Todo[]): Promise {
    try {
      await AsyncStorage.setItem(TODOSKEY, JSON.stringify(todos));
    } catch (error) {
      console.error('Erreur sauvegarde todos:', error);
      throw new Error('Impossible de sauvegarder les tâches');
    }
  }

  async getTodoById(id: string): Promise {
    try {
      const todos = await this.getTodos();
      return todos.find(todo => todo.id === id) || null;
    } catch (error) {
      console.error('Erreur récupération todo:', error);
      return null;
    }
  }

  async addTodo(todo: Todo): Promise {
    try {
      const todos = await this.getTodos();
      const newTodos = [...todos, todo];
      await this.saveTodos(newTodos);
      return todo;
    } catch (error) {
      console.error('Erreur ajout todo:', error);
      throw new Error('Impossible d'ajouter la tâche');
    }
  }

  async updateTodo(id: string, updates: Partial): Promise {
    try {
      const todos = await this.getTodos();
      const index = todos.findIndex(todo => todo.id === id);

      if (index === -1) {
        throw new Error('Tâche introuvable');
      }

      const updatedTodo = {
        ...todos[index],
        ...updates,
        updatedAt: new Date().toISOString(),
      };

      todos[index] = updatedTodo;
      await this.saveTodos(todos);
      return updatedTodo;
    } catch (error) {
      console.error('Erreur mise à jour todo:', error);
      throw new Error('Impossible de mettre à jour la tâche');
    }
  }

  async deleteTodo(id: string): Promise {
    try {
      const todos = await this.getTodos();
      const filteredTodos = todos.filter(todo => todo.id !== id);
      await this.saveTodos(filteredTodos);
    } catch (error) {
      console.error('Erreur suppression todo:', error);
      throw new Error('Impossible de supprimer la tâche');
    }
  }

  async toggleTodoComplete(id: string): Promise {
    try {
      const todos = await this.getTodos();
      const todo = todos.find(t => t.id === id);

      if (!todo) {
        throw new Error('Tâche introuvable');
      }

      return await this.updateTodo(id, {
        completed: !todo.completed,
      });
    } catch (error) {
      console.error('Erreur toggle todo:', error);
      throw new Error('Impossible de modifier le statut');
    }
  }

  // Gestion des catégories
  async getCategories(): Promise {
    try {
      const categoriesJson = await AsyncStorage.getItem(CATEGORIESKEY);

      if (!categoriesJson) {
        const defaultCategories = this.getDefaultCategories();
        await this.saveCategories(defaultCategories);
        return defaultCategories;
      }

      return JSON.parse(categoriesJson);
    } catch (error) {
      console.error('Erreur lecture catégories:', error);
      return this.getDefaultCategories();
    }
  }

  async saveCategories(categories: TodoCategory[]): Promise {
    try {
      await AsyncStorage.setItem(CATEGORIESKEY, JSON.stringify(categories));
    } catch (error) {
      console.error('Erreur sauvegarde catégories:', error);
      throw new Error('Impossible de sauvegarder les catégories');
    }
  }

  private getDefaultCategories(): TodoCategory[] {
    return [
      { id: '1', name: 'Personnel', color: '#3B82F6', icon: 'person' },
      { id: '2', name: 'Travail', color: '#10B981', icon: 'briefcase' },
      { id: '3', name: 'Shopping', color: '#F59E0B', icon: 'cart' },
      { id: '4', name: 'Santé', color: '#EF4444', icon: 'heart' },
    ];
  }

  // Utilitaires
  async clearAllData(): Promise {
    try {
      await AsyncStorage.multiRemove([TODOSKEY, CATEGORIESKEY]);
    } catch (error) {
      console.error('Erreur suppression données:', error);
      throw new Error('Impossible de supprimer les données');
    }
  }
}

export default new StorageService();

3.3 Gestion d’état avec Zustand

// src/store/todoStore.ts
import { create } from 'zustand';
import { Todo, TodoFilter, TodoSort } from '@/types/todo';
import storageService from '@/services/storage';
import uuid from 'react-native-uuid';

interface TodoState {
  todos: Todo[];
  loading: boolean;
  error: string | null;
  filter: TodoFilter;
  sortBy: TodoSort;

  // Actions
  loadTodos: () => Promise;
  addTodo: (
    title: string,
    description: string,
    priority: Todo['priority'],
    dueDate: string | null,
    category: string
  ) => Promise;
  updateTodo: (id: string, updates: Partial) => Promise;
  deleteTodo: (id: string) => Promise;
  toggleComplete: (id: string) => Promise;
  setFilter: (filter: TodoFilter) => void;
  setSortBy: (sort: TodoSort) => void;
  clearError: () => void;

  // Getters
  getFilteredTodos: () => Todo[];
  getTodosByCategory: (category: string) => Todo[];
  getStats: () => {
    total: number;
    completed: number;
    active: number;
    overdue: number;
  };
}

export const useTodoStore = create((set, get) => ({
  todos: [],
  loading: false,
  error: null,
  filter: 'all',
  sortBy: 'date',

  loadTodos: async () => {
    set({ loading: true, error: null });

    try {
      const todos = await storageService.getTodos();
      set({ todos, loading: false });
    } catch (error) {
      set({
        error: error instanceof Error ? error.message : 'Erreur de chargement',
        loading: false,
      });
    }
  },

  addTodo: async (title, description, priority, dueDate, category) => {
    set({ loading: true, error: null });

    try {
      const newTodo: Todo = {
        id: uuid.v4() as string,
        title,
        description,
        completed: false,
        priority,
        dueDate,
        category,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      };

      await storageService.addTodo(newTodo);
      const todos = await storageService.getTodos();
      set({ todos, loading: false });
    } catch (error) {
      set({
        error: error instanceof Error ? error.message : 'Erreur d'ajout',
        loading: false,
      });
    }
  },

  updateTodo: async (id, updates) => {
    set({ loading: true, error: null });

    try {
      await storageService.updateTodo(id, updates);
      const todos = await storageService.getTodos();
      set({ todos, loading: false });
    } catch (error) {
      set({
        error: error instanceof Error ? error.message : 'Erreur de mise à jour',
        loading: false,
      });
    }
  },

  deleteTodo: async (id) => {
    set({ loading: true, error: null });

    try {
      await storageService.deleteTodo(id);
      const todos = await storageService.getTodos();
      set({ todos, loading: false });
    } catch (error) {
      set({
        error: error instanceof Error ? error.message : 'Erreur de suppression',
        loading: false,
      });
    }
  },

  toggleComplete: async (id) => {
    try {
      await storageService.toggleTodoComplete(id);
      const todos = await storageService.getTodos();
      set({ todos });
    } catch (error) {
      set({
        error: error instanceof Error ? error.message : 'Erreur de modification',
      });
    }
  },

  setFilter: (filter) => set({ filter }),

  setSortBy: (sortBy) => set({ sortBy }),

  clearError: () => set({ error: null }),

  getFilteredTodos: () => {
    const { todos, filter, sortBy } = get();
    let filtered = todos;

    // Filtrage
    if (filter === 'active') {
      filtered = todos.filter(todo => !todo.completed);
    } else if (filter === 'completed') {
      filtered = todos.filter(todo => todo.completed);
    }

    // Tri
    const sorted = [...filtered].sort((a, b) => {
      switch (sortBy) {
        case 'date':
          return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();

        case 'priority':
          const priorityOrder = { high: 0, medium: 1, low: 2 };
          return priorityOrder[a.priority] - priorityOrder[b.priority];

        case 'alphabetical':
          return a.title.localeCompare(b.title);

        default:
          return 0;
      }
    });

    return sorted;
  },

  getTodosByCategory: (category) => {
    const { todos } = get();
    return todos.filter(todo => todo.category === category);
  },

  getStats: () => {
    const { todos } = get();
    const now = new Date();

    return {
      total: todos.length,
      completed: todos.filter(t => t.completed).length,
      active: todos.filter(t => !t.completed).length,
      overdue: todos.filter(t => {
        if (!t.dueDate || t.completed) return false;
        return new Date(t.dueDate) < now;
      }).length,
    };
  },
}));

4. Composants UI réutilisables

4.1 Composant TodoCard

// src/components/TodoCard.tsx
import React from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  Animated,
} from 'react-native';
import { Todo } from '@/types/todo';
import { format, isPast } from 'date-fns';
import { fr } from 'date-fns/locale';

interface TodoCardProps {
  todo: Todo;
  onPress: () => void;
  onToggleComplete: () => void;
  onDelete: () => void;
}

export const TodoCard: React.FC = ({
  todo,
  onPress,
  onToggleComplete,
  onDelete,
}) => {
  const scaleAnim = React.useRef(new Animated.Value(1)).current;

  const handlePressIn = () => {
    Animated.spring(scaleAnim, {
      toValue: 0.97,
      useNativeDriver: true,
    }).start();
  };

  const handlePressOut = () => {
    Animated.spring(scaleAnim, {
      toValue: 1,
      useNativeDriver: true,
    }).start();
  };

  const getPriorityColor = () => {
    switch (todo.priority) {
      case 'high':
        return '#EF4444';
      case 'medium':
        return '#F59E0B';
      case 'low':
        return '#10B981';
      default:
        return '#6B7280';
    }
  };

  const isOverdue = todo.dueDate && isPast(new Date(todo.dueDate)) && !todo.completed;

  return (
    
      
        
          
            
              {todo.completed && }
            
          

          
            
              {todo.title}
            

            {todo.description ? (
              
                {todo.description}
              
            ) : null}

            
              
                
                  {todo.priority === 'high' && 'Haute'}
                  {todo.priority === 'medium' && 'Moyenne'}
                  {todo.priority === 'low' && 'Basse'}
                
              

              {todo.dueDate && (
                
                  {format(new Date(todo.dueDate), 'dd MMM yyyy', { locale: fr })}
                
              )}
            
          

          
            ×
          
        
      
    
  );
};

const styles = StyleSheet.create({
  container: {
    marginBottom: 12,
  },
  card: {
    backgroundColor: '#FFFFFF',
    borderRadius: 12,
    padding: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  completedCard: {
    opacity: 0.7,
  },
  header: {
    flexDirection: 'row',
    alignItems: 'flex-start',
  },
  checkboxContainer: {
    marginRight: 12,
  },
  checkbox: {
    width: 24,
    height: 24,
    borderRadius: 12,
    borderWidth: 2,
    borderColor: '#D1D5DB',
    alignItems: 'center',
    justifyContent: 'center',
  },
  checkboxChecked: {
    backgroundColor: '#3B82F6',
    borderColor: '#3B82F6',
  },
  checkmark: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: 'bold',
  },
  content: {
    flex: 1,
  },
  title: {
    fontSize: 16,
    fontWeight: '600',
    color: '#1F2937',
    marginBottom: 4,
  },
  completedTitle: {
    textDecorationLine: 'line-through',
    color: '#9CA3AF',
  },
  description: {
    fontSize: 14,
    color: '#6B7280',
    marginBottom: 8,
  },
  metadata: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
  },
  priorityBadge: {
    paddingHorizontal: 8,
    paddingVertical: 4,
    borderRadius: 4,
  },
  priorityText: {
    color: '#FFFFFF',
    fontSize: 12,
    fontWeight: '600',
  },
  dueDate: {
    fontSize: 12,
    color: '#6B7280',
  },
  overdue: {
    color: '#EF4444',
    fontWeight: '600',
  },
  deleteButton: {
    marginLeft: 8,
  },
  deleteIcon: {
    fontSize: 28,
    color: '#EF4444',
    fontWeight: '300',
  },
});

4.2 Composant Button personnalisé

// src/components/Button.tsx
import React from 'react';
import {
  TouchableOpacity,
  Text,
  StyleSheet,
  ActivityIndicator,
  ViewStyle,
  TextStyle,
} from 'react-native';

interface ButtonProps {
  title: string;
  onPress: () => void;
  variant?: 'primary' | 'secondary' | 'outline' | 'danger';
  size?: 'small' | 'medium' | 'large';
  loading?: boolean;
  disabled?: boolean;
  style?: ViewStyle;
  textStyle?: TextStyle;
  fullWidth?: boolean;
}

export const Button: React.FC = ({
  title,
  onPress,
  variant = 'primary',
  size = 'medium',
  loading = false,
  disabled = false,
  style,
  textStyle,
  fullWidth = false,
}) => {
  const buttonStyles = [
    styles.button,
    styles[${variant}Button],
    styles[${size}Button],
    fullWidth && styles.fullWidth,
    (disabled || loading) && styles.disabled,
    style,
  ];

  const textStyles = [
    styles.text,
    styles[${variant}Text],
    styles[${size}Text],
    textStyle,
  ];

  return (
    
      {loading ? (
        
      ) : (
        {title}
      )}
    
  );
};

const styles = StyleSheet.create({
  button: {
    borderRadius: 8,
    alignItems: 'center',
    justifyContent: 'center',
  },
  fullWidth: {
    width: '100%',
  },
  disabled: {
    opacity: 0.5,
  },

  // Variants
  primaryButton: {
    backgroundColor: '#3B82F6',
  },
  secondaryButton: {
    backgroundColor: '#6B7280',
  },
  outlineButton: {
    backgroundColor: 'transparent',
    borderWidth: 2,
    borderColor: '#3B82F6',
  },
  dangerButton: {
    backgroundColor: '#EF4444',
  },

  // Sizes
  smallButton: {
    paddingHorizontal: 12,
    paddingVertical: 6,
  },
  mediumButton: {
    paddingHorizontal: 16,
    paddingVertical: 12,
  },
  largeButton: {
    paddingHorizontal: 24,
    paddingVertical: 16,
  },

  // Text styles
  text: {
    fontWeight: '600',
  },
  primaryText: {
    color: '#FFFFFF',
  },
  secondaryText: {
    color: '#FFFFFF',
  },
  outlineText: {
    color: '#3B82F6',
  },
  dangerText: {
    color: '#FFFFFF',
  },
  smallText: {
    fontSize: 14,
  },
  mediumText: {
    fontSize: 16,
  },
  largeText: {
    fontSize: 18,
  },
});

5. Navigation avec React Navigation

5.1 Configuration du navigateur

// src/navigation/AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { RootStackParamList } from '@/types/navigation';

// Screens
import TodoListScreen from '@/screens/TodoListScreen';
import TodoDetailScreen from '@/screens/TodoDetailScreen';
import AddTodoScreen from '@/screens/AddTodoScreen';
import EditTodoScreen from '@/screens/EditTodoScreen';
import SettingsScreen from '@/screens/SettingsScreen';

const Stack = createStackNavigator();

export const AppNavigator: React.FC = () => {
  return (
    
       {
            return {
              cardStyle: {
                transform: [
                  {
                    translateX: current.progress.interpolate({
                      inputRange: [0, 1],
                      outputRange: [layouts.screen.width, 0],
                    }),
                  },
                ],
              },
            };
          },
        }}
      >
        

        

        

        

        
      
    
  );
};

6. Écrans principaux

6.1 Écran Liste des tâches

// src/screens/TodoListScreen.tsx
import React, { useEffect, useState } from 'react';
import {
  View,
  FlatList,
  StyleSheet,
  Text,
  TouchableOpacity,
  RefreshControl,
  Alert,
  SafeAreaView,
} from 'react-native';
import { TodoListNavigationProp } from '@/types/navigation';
import { useTodoStore } from '@/store/todoStore';
import { TodoCard } from '@/components/TodoCard';
import { Button } from '@/components/Button';
import { Todo, TodoFilter, TodoSort } from '@/types/todo';

interface Props {
  navigation: TodoListNavigationProp;
}

const TodoListScreen: React.FC = ({ navigation }) => {
  const {
    loadTodos,
    deleteTodo,
    toggleComplete,
    getFilteredTodos,
    getStats,
    setFilter,
    setSortBy,
    filter,
    sortBy,
    loading,
    error,
    clearError,
  } = useTodoStore();

  const [refreshing, setRefreshing] = useState(false);

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

  useEffect(() => {
    if (error) {
      Alert.alert('Erreur', error, [
        { text: 'OK', onPress: clearError },
      ]);
    }
  }, [error]);

  const handleRefresh = async () => {
    setRefreshing(true);
    await loadTodos();
    setRefreshing(false);
  };

  const handleDeleteTodo = (id: string) => {
    Alert.alert(
      'Confirmation',
      'Voulez-vous vraiment supprimer cette tâche ?',
      [
        { text: 'Annuler', style: 'cancel' },
        {
          text: 'Supprimer',
          style: 'destructive',
          onPress: () => deleteTodo(id),
        },
      ]
    );
  };

  const todos = getFilteredTodos();
  const stats = getStats();

  const renderTodo = ({ item }: { item: Todo }) => (
     navigation.navigate('TodoDetail', { todoId: item.id })}
      onToggleComplete={() => toggleComplete(item.id)}
      onDelete={() => handleDeleteTodo(item.id)}
    />
  );

  const renderHeader = () => (
    
      
        
          {stats.total}
          Total
        

        
          
            {stats.completed}
          
          Terminées
        

        
          
            {stats.active}
          
          Actives
        

        {stats.overdue > 0 && (
          
            
              {stats.overdue}
            
            En retard
          
        )}
      

      
         setFilter('all')}
        >
          
            Toutes
          
        

         setFilter('active')}
        >
          
            Actives
          
        

         setFilter('completed')}
        >
          
            Terminées
          
        
      

      
        Trier par :

         setSortBy('date')}
        >
          Date
        

         setSortBy('priority')}
        >
          Priorité
        

         setSortBy('alphabetical')}
        >
          A-Z
        
      
    
  );

  const renderEmpty = () => (
    
      📝
      Aucune tâche
      
        {filter === 'all'
          ? 'Ajoutez votre première tâche'
          : filter === 'active'
          ? 'Aucune tâche active'
          : 'Aucune tâche terminée'}
      
    
  );

  return (
    
       item.id}
        contentContainerStyle={styles.listContent}
        ListHeaderComponent={renderHeader}
        ListEmptyComponent={renderEmpty}
        refreshControl={
          
        }
      />

      
        
    
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F3F4F6',
  },
  listContent: {
    padding: 16,
    paddingBottom: 80,
  },
  header: {
    marginBottom: 16,
  },
  statsContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    backgroundColor: '#FFFFFF',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  statItem: {
    alignItems: 'center',
  },
  statValue: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#1F2937',
  },
  statLabel: {
    fontSize: 12,
    color: '#6B7280',
    marginTop: 4,
  },
  filters: {
    flexDirection: 'row',
    gap: 8,
    marginBottom: 16,
  },
  filterButton: {
    flex: 1,
    paddingVertical: 8,
    paddingHorizontal: 12,
    backgroundColor: '#FFFFFF',
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#E5E7EB',
    alignItems: 'center',
  },
  activeFilter: {
    backgroundColor: '#3B82F6',
    borderColor: '#3B82F6',
  },
  filterText: {
    fontSize: 14,
    color: '#6B7280',
    fontWeight: '500',
  },
  activeFilterText: {
    color: '#FFFFFF',
  },
  sortContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
  },
  sortLabel: {
    fontSize: 14,
    color: '#6B7280',
    fontWeight: '500',
  },
  sortButton: {
    paddingVertical: 6,
    paddingHorizontal: 12,
    backgroundColor: '#E5E7EB',
    borderRadius: 6,
  },
  activeSort: {
    backgroundColor: '#3B82F6',
  },
  sortText: {
    fontSize: 12,
    color: '#1F2937',
    fontWeight: '500',
  },
  emptyContainer: {
    alignItems: 'center',
    justifyContent: 'center',
    paddingVertical: 60,
  },
  emptyEmoji: {
    fontSize: 64,
    marginBottom: 16,
  },
  emptyTitle: {
    fontSize: 20,
    fontWeight: '600',
    color: '#1F2937',
    marginBottom: 8,
  },
  emptyText: {
    fontSize: 14,
    color: '#6B7280',
  },
  fab: {
    position: 'absolute',
    bottom: 16,
    left: 16,
    right: 16,
  },
});

export default TodoListScreen;

6.2 Écran d'ajout de tâche

// src/screens/AddTodoScreen.tsx
import React, { useState } from 'react';
import {
  View,
  TextInput,
  StyleSheet,
  ScrollView,
  Text,
  TouchableOpacity,
  Alert,
  Platform,
  SafeAreaView,
  KeyboardAvoidingView,
} from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker';
import { AddTodoNavigationProp } from '@/types/navigation';
import { useTodoStore } from '@/store/todoStore';
import { Button } from '@/components/Button';
import { Todo } from '@/types/todo';

interface Props {
  navigation: AddTodoNavigationProp;
}

const AddTodoScreen: React.FC = ({ navigation }) => {
  const { addTodo, loading } = useTodoStore();

  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [priority, setPriority] = useState('medium');
  const [dueDate, setDueDate] = useState(null);
  const [showDatePicker, setShowDatePicker] = useState(false);
  const [category, setCategory] = useState('Personnel');

  const categories = ['Personnel', 'Travail', 'Shopping', 'Santé'];

  const handleSubmit = async () => {
    if (!title.trim()) {
      Alert.alert('Erreur', 'Le titre est obligatoire');
      return;
    }

    try {
      await addTodo(
        title.trim(),
        description.trim(),
        priority,
        dueDate ? dueDate.toISOString() : null,
        category
      );

      Alert.alert('Succès', 'Tâche ajoutée avec succès', [
        { text: 'OK', onPress: () => navigation.goBack() },
      ]);
    } catch (error) {
      Alert.alert('Erreur', 'Impossible d'ajouter la tâche');
    }
  };

  const handleDateChange = (event: any, selectedDate?: Date) => {
    setShowDatePicker(Platform.OS === 'ios');

    if (selectedDate) {
      setDueDate(selectedDate);
    }
  };

  return (
    
      
        
          
            Titre 
            
          

          
            Description
            
          

          
            Priorité
            
               setPriority('low')}
              >
                
                  Basse
                
              

               setPriority('medium')}
              >
                
                  Moyenne
                
              

               setPriority('high')}
              >
                
                  Haute
                
              
            
          

          
            Catégorie
            
              {categories.map((cat) => (
                 setCategory(cat)}
                >
                  
                    {cat}
                  
                
              ))}
            
          

          
            Date d'échéance
             setShowDatePicker(true)}
            >
              
                {dueDate
                  ? dueDate.toLocaleDateString('fr-FR', {
                      day: 'numeric',
                      month: 'long',
                      year: 'numeric',
                    })
                  : 'Sélectionner une date'}
              
            

            {dueDate && (
               setDueDate(null)}
              >
                Supprimer la date
              
            )}

            {showDatePicker && (
              
            )}
          

          
            
          
        
      
    
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F3F4F6',
  },
  flex: {
    flex: 1,
  },
  content: {
    padding: 16,
  },
  section: {
    marginBottom: 24,
  },
  label: {
    fontSize: 16,
    fontWeight: '600',
    color: '#1F2937',
    marginBottom: 8,
  },
  input: {
    backgroundColor: '#FFFFFF',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    color: '#1F2937',
    borderWidth: 1,
    borderColor: '#E5E7EB',
  },
  textArea: {
    minHeight: 100,
    paddingTop: 12,
  },
  priorityContainer: {
    flexDirection: 'row',
    gap: 12,
  },
  priorityButton: {
    flex: 1,
    paddingVertical: 12,
    paddingHorizontal: 16,
    backgroundColor: '#FFFFFF',
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#E5E7EB',
    alignItems: 'center',
  },
  priorityText: {
    fontSize: 14,
    fontWeight: '600',
    color: '#6B7280',
  },
  priorityTextActive: {
    color: '#FFFFFF',
  },
  categoryContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 8,
  },
  categoryButton: {
    paddingVertical: 8,
    paddingHorizontal: 16,
    backgroundColor: '#FFFFFF',
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#E5E7EB',
  },
  categoryButtonActive: {
    backgroundColor: '#3B82F6',
    borderColor: '#3B82F6',
  },
  categoryText: {
    fontSize: 14,
    fontWeight: '500',
    color: '#6B7280',
  },
  categoryTextActive: {
    color: '#FFFFFF',
  },
  dateButton: {
    backgroundColor: '#FFFFFF',
    borderRadius: 8,
    padding: 12,
    borderWidth: 1,
    borderColor: '#E5E7EB',
  },
  dateButtonText: {
    fontSize: 16,
    color: '#1F2937',
  },
  clearDateButton: {
    marginTop: 8,
    alignSelf: 'flex-start',
  },
  clearDateText: {
    fontSize: 14,
    color: '#EF4444',
  },
  actions: {
    flexDirection: 'row',
    gap: 12,
    marginTop: 16,
  },
  actionButton: {
    flex: 1,
  },
});

export default AddTodoScreen;

7. Lancement de l'application

7.1 Démarrage de l'app

# iOS (macOS uniquement)
npm run ios
# ou pour un simulateur spécifique
npx react-native run-ios --simulator="iPhone 15 Pro"

# Android
npm run android
# ou pour un émulateur spécifique
npx react-native run-android --deviceId=emulator-5554

# Démarrer Metro Bundler séparément
npm start

7.2 Débogage

// Configuration Reactotron (optionnel)
// npm install --save-dev reactotron-react-native

// ReactotronConfig.ts
import Reactotron from 'reactotron-react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

if (DEV) {
  Reactotron
    .setAsyncStorageHandler(AsyncStorage)
    .configure({
      name: 'TodoApp',
    })
    .useReactNative({
      asyncStorage: true,
      networking: {
        ignoreUrls: /symbolicate/,
      },
      editor: false,
      errors: { veto: (stackFrame) => false },
      overlay: false,
    })
    .connect();
}

export default Reactotron;

7.3 Tests avec Jest

// tests/TodoCard.test.tsx
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { TodoCard } from '@/components/TodoCard';
import { Todo } from '@/types/todo';

const mockTodo: Todo = {
  id: '1',
  title: 'Test Todo',
  description: 'Test Description',
  completed: false,
  priority: 'medium',
  dueDate: null,
  category: 'Personnel',
  createdAt: new Date().toISOString(),
  updatedAt: new Date().toISOString(),
};

describe('TodoCard', () => {
  it('affiche correctement le titre et la description', () => {
    const { getByText } = render(
      
    );

    expect(getByText('Test Todo')).toBeTruthy();
    expect(getByText('Test Description')).toBeTruthy();
  });

  it('appelle onToggleComplete quand on clique sur la checkbox', () => {
    const onToggleComplete = jest.fn();

    const { getByTestId } = render(
      
    );

    fireEvent.press(getByTestId('checkbox'));
    expect(onToggleComplete).toHaveBeenCalledTimes(1);
  });
});

8. Build et déploiement

8.1 Build Android (APK)

# Générer une clé de signature (première fois uniquement)
cd android/app
keytool -genkeypair -v -storetype PKCS12 -keystore todo-app-release.keystore -alias todo-app -keyalg RSA -keysize 2048 -validity 10000

# Configurer gradle.properties
# android/gradle.properties
MYAPPRELEASESTOREFILE=todo-app-release.keystore
MYAPPRELEASEKEYALIAS=todo-app
MYAPPRELEASESTOREPASSWORD=yourpassword
MYAPPRELEASEKEYPASSWORD=your_password

# Build release
cd android
./gradlew assembleRelease

# APK disponible dans :
# android/app/build/outputs/apk/release/app-release.apk

8.2 Build iOS (IPA)

# Ouvrir Xcode
open ios/TodoApp.xcworkspace

# Dans Xcode :
# 1. Sélectionner le schéma "TodoApp" > "Generic iOS Device"
# 2. Product > Archive
# 3. Distribute App > App Store Connect
# 4. Suivre l'assistant de publication

Conclusion

Vous avez maintenant une application React Native complète et fonctionnelle. Ce guide couvre les fondamentaux du développement mobile avec React Native : configuration, navigation, gestion d'état, stockage local et interface utilisateur.

Prochaines étapes :

  • Ajouter des tests end-to-end avec Detox
  • Intégrer des notifications push
  • Implémenter la synchronisation cloud
  • Optimiser les performances avec React.memo et useMemo
  • Ajouter l'internationalisation (i18n)
  • Ressources utiles :**

  • Documentation officielle : https://reactnative.dev
  • React Navigation : https://reactnavigation.org
  • Awesome React Native : https://github.com/jondot/awesome-react-native
  • Continuez à pratiquer et à explorer l'écosystème React Native pour créer des applications mobiles professionnelles.

    Une remarque, un retour ?

    Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.