Le projet consiste à créer une application multiplateforme de type Todo Liste avec les particularités suivantes:
Dans le command line de windows (CMD), nous allons nous positioner dans notre répertoire de travail et taper la commande suivante:
npx create-expo-app react-native-todolist
Ceci aura pour effet de créer un nouveau projet :
À l'aide de la commande cd, nous allons nous déplacer dans notre projet et démarrer celui-ci
cd react-native-todolist
npm run web
Le résultat sur le mobile:
Nous allons faire un reset de l'application:
npm run reset-project
Vous devriez avoir le résultat suivant:
Nous pouvons exécuter l'étape 1 de l'image ci-dessus:
npx expo start
Le résultat devrait ressembler à ceci:
Et sur le mobile:
Finalement, nous pouvons ouvrir le code dans VSCode (File - Add Folder to Workspace…):
Finalement, il faut rajouter le Safe-Area-Context (afin d'assurer que la app n'empiète pas sur le notch du téléphone):
npm i react-native-safe-area-context
Si l'installation se passe bien, nous devrions avoir dans le package.json le fichier suivant:
Nous pouvons maintenant importer le composant et le mettre dans notre application de base (index.tsx):
Voici le code:
import { Text, View } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
export default function Index() {
return (
<SafeAreaProvider>
<SafeAreaView>
<Text>Hello</Text>
</SafeAreaView>
</SafeAreaProvider>
);
}
La méthode map()
est une fonction très utile qui permet de transformer chaque élément d'un tableau pour créer un nouveau tableau. Elle parcourt le tableau original et applique une fonction de transformation à chaque élément.
const nombres = [1, 2, 3, 4, 5];
const doubles = nombres.map(nombre => nombre * 2);
// doubles est maintenant [2, 4, 6, 8, 10]
Voici un exemple visuel de la fonction:
La fonction map prend en paramètre une fonction de callback qui peut recevoir jusqu'à trois arguments :
Voici un exemple plus complet :
const personnes = [
{ nom: 'Pierre', age: 25 },
{ nom: 'Marie', age: 30 },
{ nom: 'Jean', age: 35 }
];
const noms = personnes.map((personne, index) => {
return `${index + 1}. ${personne.nom}`;
});
// noms est maintenant ['1. Pierre', '2. Marie', '3. Jean']
Points importants à retenir :
map()
crée toujours un nouveau tableau et ne modifie pas le tableau originalLa méthode find()
en JavaScript est une fonction qui permet de rechercher et retourner le premier élément d'un tableau qui satisfait une condition donnée.
Voici sa syntaxe de base :
const element = tableau.find(element => condition);
Pour les gens plus visuel, voici un exemple de find:
Quelques exemples concrets :
// Exemple 1 : Trouver le premier nombre supérieur à 10
const nombres = [5, 8, 12, 15, 20];
const premierNombreSup10 = nombres.find(nombre => nombre > 10);
console.log(premierNombreSup10); // Affiche : 12
// Exemple 2 : Trouver un utilisateur par son id
const utilisateurs = [
{ id: 1, nom: 'Pierre' },
{ id: 2, nom: 'Marie' },
{ id: 3, nom: 'Jean' }
];
const utilisateurRecherche = utilisateurs.find(user => user.id === 2);
console.log(utilisateurRecherche); // Affiche : { id: 2, nom: 'Marie' }
Points importants à noter :
find()
retourne le premier élément qui correspond à la conditionundefined
Voici un dernier exemple plus spécifique:
// Dans une application de gestion de tâches
const taches = [
{ id: 1, titre: 'Courses', complete: false },
{ id: 2, titre: 'Sport', complete: true },
{ id: 3, titre: 'Lecture', complete: false }
];
// Rechercher une tâche spécifique
const tacheRecherchee = taches.find(tache => tache.id === 2);
console.log(tacheRecherchee); // Affiche : { id: 2, titre: 'Sport', complete: true }
La différence avec
filter()
est quefind()
ne retourne que le premier élément trouvé, alors quefilter()
retourne tous les éléments qui correspondent à la condition dans un nouveau tableau.
La fonction reduce()
est une méthode qui permet de réduire un tableau à une seule valeur. Elle parcourt chaque élément du tableau et accumule un résultat.
Voici sa syntaxe de base :
array.reduce((accumulateur, elementCourant) => {
// logique de réduction
return nouvelleValeurAccumulateur;
}, valeurInitiale);
Voici une représentation visuelle de la fonction:
Voici quelques exemples concrets :
1. Somme des nombres d'un tableau :
const nombres = [1, 2, 3, 4, 5];
const somme = nombres.reduce((acc, curr) => acc + curr, 0);
console.log(somme); // 15
2. Création d'un objet à partir d'un tableau :
const fruits = ['pomme', 'banane', 'pomme', 'orange', 'banane'];
const compteur = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(compteur); // { pomme: 2, banane: 2, orange: 1 }
3. Calcul du total d'un panier d'achats :
const panier = [
{ produit: 'T-shirt', prix: 15 },
{ produit: 'Pantalon', prix: 25 },
{ produit: 'Chaussures', prix: 50 }
];
const total = panier.reduce((acc, item) => acc + item.prix, 0);
console.log(total); // 90
Points importants à noter :
reduce()
peut transformer un tableau en n'importe quel type de valeur (nombre, string, objet, tableau...)La fonction filter()
en JavaScript permet de créer un nouveau tableau contenant tous les éléments qui satisfont une condition donnée.
Voici sa syntaxe de base :
const nouveauTableau = tableau.filter(element => condition);
Exemples concrets :
1. Filtrer les nombres pairs :
const nombres = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const nombresPairs = nombres.filter(nombre => nombre % 2 === 0);
console.log(nombresPairs); // [2, 4, 6, 8, 10]
2. Filtrer des objets selon une propriété :
const utilisateurs = [
{ nom: 'Pierre', age: 25 },
{ nom: 'Marie', age: 17 },
{ nom: 'Jean', age: 30 },
{ nom: 'Sophie', age: 15 }
];
// Filtrer les utilisateurs majeurs
const utilisateursMajeurs = utilisateurs.filter(user => user.age >= 18);
console.log(utilisateursMajeurs);
// [{ nom: 'Pierre', age: 25 }, { nom: 'Jean', age: 30 }]
3. Filtrer les tâches non complétées :
const taches = [
{ id: 1, titre: 'Courses', complete: true },
{ id: 2, titre: 'Sport', complete: false },
{ id: 3, titre: 'Lecture', complete: false }
];
const tachesIncompletes = taches.filter(tache => !tache.complete);
console.log(tachesIncompletes);
// [{ id: 2, titre: 'Sport', complete: false },
// { id: 3, titre: 'Lecture', complete: false }]
Points importants :
filter()
crée toujours un nouveau tableauPour rappel, la différence principale avec find()
est que filter()
retourne tous les éléments qui correspondent à la condition, alors que find()
ne retourne que le premier élément trouvé.
La fonction Alert
dans React Native est un système de notification qui permet d'afficher des boîtes de dialogue modales natives sur iOS et Android.
Voici sa syntaxe de base :
Alert.alert(
titre, // String
message, // String (optionnel)
boutons, // Array de boutons (optionnel)
options // Objet d'options (optionnel)
);
Exemples concrets :
Alert.alert(
"Succès",
"La tâche a été créée avec succès",
[
{ text: "OK" }
]
);
2. Alert avec plusieurs boutons :
Alert.alert(
"Supprimer la tâche",
"Êtes-vous sûr de vouloir supprimer cette tâche ?",
[
{
text: "Annuler",
style: "cancel"
},
{
text: "Supprimer",
onPress: () => supprimerTache(id),
style: "destructive" // Rouge sur iOS
}
]
);
3. Alert avec callback :
Alert.alert(
"Déconnexion",
"Voulez-vous vraiment vous déconnecter ?",
[
{
text: "Non",
onPress: () => console.log("Annulation"),
style: "cancel"
},
{
text: "Oui",
onPress: () => {
// Code de déconnexion
logout();
navigation.navigate('Login');
}
}
]
);
Points importants :
onPress
pour gérer le clicL'Alert est utile pour :
Pour plus d'info sur la fonction Alert: https://reactnative.dev/docs/alert
Le layout est séparé en 3 parties:
Nous allons créer un fichier de style vide (App.style.js) avec le code suivant:
Voici le code:
//App.style.js
import { StyleSheet } from 'react-native';
export const s = StyleSheet.create({
app: { backgroundColor: 'F9F9F9' },
header: { backgroundColor: 'red' },
body: { backgroundColor: 'blue' },
footer: { backgroundColor: 'green' },
});
Dans index.tsx, nous allons rajouter nos 3 views (ne pas oublier d'importer le style!):
//index.tsx
import { Text, View } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { s } from '../App.style';
export default function Index() {
return (
<SafeAreaProvider>
<SafeAreaView style={s.app}>
<View style={s.header}>
<Text>Header</Text>
</View>
<View style={s.body}>
<Text>Body</Text>
</View>
<View style={s.footer}>
<Text>Footer</Text>
</View>
</SafeAreaView>
</SafeAreaProvider>
);
}
Le résultat sur mobile:
Nous allons maintenant jouer avec les flexbox afin d'avoir une division similaire à celle-ci:
Android
Iphone:
Le code à rajouter dans App.style.js sera:
//App.style.js
import { StyleSheet } from 'react-native';
export const s = StyleSheet.create({
app: { flex: 1, backgroundColor: 'F9F9F9' },
header: { flex: 1, backgroundColor: 'red' },
body: { flex: 5, backgroundColor: 'blue' },
footer: { flex: 1, backgroundColor: 'green' },
});
Pour les iphones, le footer empiète avec le SafeAreaView. Nous allons donc sortir celui-ci de la SafeAreaView et rajotuer des fragment parent:
Voici le code dans index.tsx:
//index.tsx
import { Text, View } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { s } from '../App.style';
export default function Index() {
return (
<>
<SafeAreaProvider>
<SafeAreaView style={s.app}>
<View style={s.header}>
<Text>Header</Text>
</View>
<View style={s.body}>
<Text>Body</Text>
</View>
</SafeAreaView>
</SafeAreaProvider>
<View style={s.footer}>
<Text>Footer</Text>
</View>
</>
);
}
En sortant le footer du SafeAreaView, les proportions avec les flexblox ne sont plus respectées. Nous allons imposer une hauteur au footer afin de garder des proptions respectables:
//App.style.js
import { StyleSheet } from 'react-native';
export const s = StyleSheet.create({
app: { flex: 1, backgroundColor: 'F9F9F9' },
header: { flex: 1, backgroundColor: 'red' },
body: { flex: 5, backgroundColor: 'blue' },
footer: { height: 70, backgroundColor: 'green' },
});
Le résultat sur Iphone est maintenant:
Nous pouvons maintenant retiré les couleurs de background:
import { StyleSheet } from 'react-native';
export const s = StyleSheet.create({
app: { flex: 1, backgroundColor: 'F9F9F9' },
header: { flex: 1 },
body: { flex: 5 },
footer: { height: 70 },
});
Pour les besoins de projet, nous allons importer les deux images suivantes:
logo.png:
splash.png
check.png
Vous pouvez importer les images avec un drag and drop de l'exporateur de fichier vers votre projet dans VSCode (assets/images/):
Nous pouvons maintenant configurer notre splash screen pour le projet. Cette action fait en sorte de prendre notre image splash.png lorsque l'application se télécharge.
Plus d'info sur le splash screen: https://docs.expo.dev/develop/user-interface/splash-screen-and-app-icon/
Comme nous avons l'habitude maintenant, nous allons nous construire un composant Header.jsx avec son style associé (Header.style.js) vide dans components/Header/:
Jusqu'à présent, nos données étaient créé au démarrage de l'application et mis dans un tableau. Nous allons mettre ces données dans l'espace (storage) du téléphone. Pour ce faire, nous allons utiliser le composant AsyncStorage (https://docs.expo.dev/versions/latest/sdk/async-storage/). L'installation se fait par la ligne de code suivante :
npx expo install @react-native-async-storage/async-storage
Nous allons importer le composant dans notre index.tsx:
import AsyncStorage from '@react-native-async-storage/async-storage';
AsyncStorage est une API de stockage local simple et asynchrone qui permet de stocker des données sous forme de paires clé-valeur de manière persistante dans l'application.
Voici un exemple de son utilisation:
// Importer AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';
// Stocker une donnée
const storeData = async (key, value) => {
try {
// Les valeurs doivent être stockées sous forme de chaînes
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Erreur lors du stockage:', error);
}
};
// Récupérer une donnée
const getData = async (key) => {
try {
const value = await AsyncStorage.getItem(key);
// Convertir la chaîne JSON en objet JavaScript
return value != null ? JSON.parse(value) : null;
} catch (error) {
console.error('Erreur lors de la récupération:', error);
return null;
}
};
// Supprimer une donnée
const removeData = async (key) => {
try {
await AsyncStorage.removeItem(key);
} catch (error) {
console.error('Erreur lors de la suppression:', error);
}
};
Points importants à retenir :
Pour les besoins de notre application, nous allons nous créer 2 fonctions ainsi que 2 variables globales afin de connaître l'état du rendering:
Deux fonctions:
2 variables globales:
La fonction saveTodoList()
C'est une fonction async
(asynchrone) permet d'utiliser await
pour gérer les opérations asynchrones
try/catch
est utilisé pour gérer les erreurs potentielles :try
:JSON.stringify(todoList)
convertit l'objet todoList
en chaîne JSONAsyncStorage.setItem()
sauvegarde cette chaîne dans le stockage local sous la clé '@todolist'await
est utilisé car setItem
est une opération asynchronecatch
:err
La fonction loadTodoList()
La fonction commence par afficher 'LOAD' dans la console (pour le débogage)
try
:await AsyncStorage.getItem('@todolist')
récupère la chaîne JSON stockée!= null
)JSON.parse()
convertit la chaîne JSON en objet JavaScriptisLoadUpdate
est mis à true
(pour indiquer que c'est un chargement initial)setTodoList()
met à jour l'état de l'application avec les données chargéescatch
:Finalement, afin de refaire un rendering à chaque modification de la todo liste, nous allons utiliser deux useEffect.
Le premier useEffect
:
[]
)loadTodoList()
pour charger les tâches depuis le stockage localuseEffect
:todoList
change (indiqué dans le tableau de dépendances [todoList]
)isLoadUpdate
est vrai (cas d'un chargement initial), il le remet à faux!isFirstRender
), sauvegarde la listeisFirstRender
comme fauxCette structure permet de :
Finalement, nous pouvons changer le code du state du todoList:
Par le code suivant: