.map(), keys, filtrage, tri et discriminated unions
Utilisez les flèches, cliquez ou glissez pour naviguer
1. Rendre des listes avec .map()
Générer du JSX dynamique
2. Comprendre les keys
Pourquoi React en a besoin
3. Filtrer et trier
De façon immutable
4. Gérer les états async
loading, error, empty, success
5. Discriminated unions
Typer les états d'UI
6. Patterns conditionnels
Affichage selon l'état
Rappel : .map() en JavaScript
Vous connaissez déjà du backend
.map() génère du JSX dynamique
La base du rendu de listes
Les keys : pourquoi et comment
Pourquoi index est un mauvais choix
Demo live : filtrage + tri
Avec useState
Discriminated unions pour les états
Lien avec DDD
Vous connaissez déjà .map() du backend!
// JavaScript classique
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
// [2, 4, 6]
.map() transforme chaque élément et retourne un nouveau tableau
En React, .map() retourne des éléments JSX!
function UserList() {
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Les accolades {} permettent d'insérer des expressions JavaScript dans JSX
React a besoin d'identifier chaque élément
Les keys aident React à réconcilier le DOM
Quand la liste change, React sait quoi mettre à jour
Bonnes keys
Mauvaises keys
// Bon - ID unique de la DB
<li key={user.id}>{user.name}</li>
// Mauvais - index du tableau
<li key={index}>{user.name}</li>
L'index change quand la liste est réordonnée ou filtrée!
Liste originale
[A, B, C]
keys: 0, 1, 2
=>
Après filtre (supprime A)
[B, C]
keys: 0, 1
React pense que B est devenu A (key 0), C est devenu B (key 1)...
Le state local des composants peut être perdu!
React affiche un warning dans la console!
Warning: Each child in a list should have a unique "key" prop.
Sans key
Avec key
filter() avant .map()
filter() crée un nouveau tableau
On ne mute jamais le tableau original!
function ProductList() {
const [products, setProducts] = useState([...]);
const [search, setSearch] = useState("");
const filtered = products.filter(p =>
p.name.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
<ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>
</>
);
}
filter() ne modifie pas products, il crée un nouveau tableau!
sort() modifie le tableau EN PLACE!
Mauvais
products.sort((a, b) => a.price - b.price);
Mute le tableau original!
Bon
[...products].sort((a, b) => a.price - b.price);
Crée une copie, puis trie
Toujours copier avant sort() : [...array].sort()
const sorted = [...products]
.filter(p => p.inStock)
.sort((a, b) => a.price - b.price)
.map(p => <ProductCard key={p.id} product={p} />);
1. filter
Garde les éléments
2. sort
Ordonne
3. map
Transforme en JSX
Chaque étape crée un nouveau tableau - jamais de mutation!
Toujours les gérer TOUS!
loading
Donnée en cours de chargement
error
Erreur lors du chargement
success
Donnée disponible
Une liste filtrée peut retourner 0 résultat...
Problème
L'utilisateur ne voit rien
Est-ce un bug? Un chargement?
Solution
Afficher un message explicite
"Aucun résultat trouvé"
if (filtered.length === 0) {
return <p>Aucun produit trouvé</p>;
}
Plusieurs états qui peuvent se chevaucher...
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
Problème : On peut être loading ET error en même temps!
Comment afficher le bon UI?
Typer les états d'UI avec TypeScript
Un type qui rend les états mutuellement exclusifs
Impossible d'être loading ET error!
type AsyncState<T> =
| { status: "loading" }
| { status: "error"; message: string }
| { status: "success"; data: T };
La propriété status est le discriminant
TypeScript sait quelles propriétés sont disponibles selon la valeur de status
const [state, setState] = useState<AsyncState<User[]>>({
status: "loading"
});
// Plus tard...
setState({ status: "success", data: users });
setState({ status: "error", message: "Échec du chargement" });
Un seul state, un seul type - impossible d'avoir des états incohérents!
function UserList({ state }: { state: AsyncState<User[]> }) {
switch (state.status) {
case "loading":
return <Spinner />;
case "error":
return <ErrorMessage>{state.message}</ErrorMessage>;
case "success":
return <ul>{state.data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
}
TypeScript sait que state.data existe seulement quand status === "success"!
Vous connaissez peut-être ce pattern du backend...
DDD : Result Type
Result<T, E>
Success(T) ou Error(E)
États mutuellement exclusifs
Discriminated Union
AsyncState<T>
loading | error | success
Même concept appliqué au frontend
Les discriminated unions apportent la rigueur du backend dans vos composants
Les erreurs à éviter
Ces erreurs sont fréquentes et compréhensibles
L'erreur
{items.map((item, index) =>
<li key={index}>{item.name}</li>
)}
L'index change quand la liste change!
La solution
{items.map(item =>
<li key={item.id}>{item.name}</li>
)}
Utilisez un ID unique et stable
L'index est acceptable SEULEMENT si la liste est statique et ne change jamais
React affiche un warning ET les performances se dégradent
Warning: Each child in a list should have a unique "key" prop.
Check the render method of `UserList`.
Sans key
React doit recréer tous les éléments
O(n) comparaisons
Avec key
React peut réutiliser les éléments
O(1) pour les éléments inchangés
sort() modifie le tableau EN PLACE - c'est rare en JS!
Mauvais
const sorted = products.sort(...);
products est muté!
Bon
const sorted = [...products].sort(...);
Copie explicite avant sort
// Méthodes qui MUTENT en JS :
push(), pop(), shift(), unshift()
splice(), sort(), reverse()
// Méthodes qui NE MUTENT PAS :
map(), filter(), reduce(), slice(), concat()
Une liste filtrée peut retourner 0 résultat...
Problème
{filtered.map(item => ...)}
Écran vide si filtered = []
L'utilisateur ne comprend pas
Solution
{filtered.length === 0 ? (
<p>Aucun résultat</p>
) : (
filtered.map(...)
)}
Message explicite
Chaque élément DOIT avoir une key unique et stable
Pas l'index! Utilisez un ID de la DB
filter() et sort() avant .map()
Ne pas oublier [...array].sort()
Les 3 états : loading, error, success
Toujours les gérer TOUS
Discriminated unions = états mutuellement exclusifs
Impossible d'être loading ET error
.map() + key
{items.map(i => <li key={i.id}>...</li>)}
Key unique et stable
Immutabilité
[...items].sort() avant .map()
Jamais de mutation
États async
loading | error | success
+ empty state!
Discriminated Union
status: "loading" | "error" | "success"
TypeScript protège
Les listes sont au coeur de la plupart des interfaces!
1. Liste de produits avec recherche
filter() + .map() avec keys correctes
2. Tri par prix (croissant/décroissant)
[...products].sort() sans mutation
3. Composant AsyncList avec discriminated union
Gérer loading, error, success, empty
4. Liste filtrable + triable + message vide
Combiner tous les concepts
Les listes et les états sont partout en React
Prochaine étape : useEffect et les effets de bord