React · Router · Markdown · Filtrage

Liste & Détail

Leçons, exercices, Markdown et navigation

Le pattern liste → dĂ©tail est le plus courant du web. Aujourd'hui on le maĂźtrise de A Ă  Z.

Objectifs de la leçon

1. Pattern liste / détail

Page liste avec filtres → page dĂ©tail avec contenu complet via React Router.

2. Grouper les données

Transformer un tableau plat en structure hiérarchique (semaine / jour).

3. Rendu Markdown

Afficher du contenu Markdown en React avec react-markdown.

4. Breadcrumbs & navigation

Se repérer dans l'arborescence avec des fils d'Ariane.

5. Filtrage et recherche cÎté client

Filtrer une liste en temps réel sans appel API supplémentaire.

Plan du cours

1

Pattern liste / détail

Page liste avec filtres → page dĂ©tail avec contenu complet.

2

Grouper par semaine / jour

Transformer un tableau plat en structure hiérarchique avec reduce().

3

Rendu Markdown cÎté client

react-markdown — afficher, styler, sĂ©curiser.

4

Navigation : breadcrumbs & URL paramétrées

/lessons/:id, retour Ă  la liste, fil d'Ariane.

5

Filtrage et recherche cÎté client

Recherche live, filtre par catégorie, état vide.

Le pattern Liste / Détail

Le plus courant du web — le maütriser est essentiel

📋

Page Liste

Toutes les leçons, avec filtres et recherche.

→

clic sur un élément

📄

Page Détail

Contenu complet de la leçon sélectionnée.

Architecture des routes React Router

// main.tsx

import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([

{

path: '/',

element: <Layout />,

children: [

{ path: 'lessons', element: <LessonList /> },

{ path: 'lessons/:id', element: <LessonDetail /> },

{ path: 'exercises', element: <ExerciseList /> },

{ path: 'exercises/:id', element: <ExerciseDetail /> },

],

},

]);

💡 :id est un paramùtre dynamique — React Router le met dans useParams().

Page Liste — structure de base

function LessonList() {

const { data: lessons, isLoading } = useQuery({

queryKey: ['lessons'],

queryFn: fetchLessons,

});

if (isLoading) return <Skeleton />;

return (

<ul>

{lessons.map(lesson => (

<li key={lesson.id}>

<Link to={`/lessons/${lesson.id}`}>

{lesson.title}

</Link>

</li>

))}

</ul>

);

}

Page DĂ©tail — useParams + React Query

function LessonDetail() {

const { id } = useParams<{ id: string }>();

const { data: lesson } = useQuery({

queryKey: ['lessons', id],

queryFn: () => fetchLesson(id!),

});

return (

<div>

<h1>{lesson?.title}</h1>

<p>{lesson?.content}</p>

</div>

);

}

💡 React Query met en cache la leçon — si elle Ă©tait dĂ©jĂ  dans la liste, pas de second appel rĂ©seau.

Grouper les données

Tableau plat → structure hiĂ©rarchique

Avant — tableau plat

{ id: 1, week: 1, title: "Variables" }

{ id: 2, week: 1, title: "Fonctions" }

{ id: 3, week: 2, title: "Classes" }

{ id: 4, week: 2, title: "OOP" }

AprĂšs — groupĂ© par semaine

Semaine 1

→ Variables, Fonctions

Semaine 2

→ Classes, OOP

Grouper avec reduce()

function groupBy<T>(

items: T[],

key: keyof T

): Record<string, T[]> {

return items.reduce((acc, item) => {

const groupKey = String(item[key]);

if (!acc[groupKey]) acc[groupKey] = [];

acc[groupKey].push(item);

return acc;

}, {} as Record<string, T[]>);

}

const grouped = groupBy(lessons, 'week');

// { "1": [...], "2": [...] }

💡 Object.entries(grouped) pour itĂ©rer sur chaque groupe dans le rendu.

Rendu de la liste groupée

const grouped = useMemo(

() => groupBy(lessons ?? [], 'week'),

[lessons]

);

return (

<div>

{Object.entries(grouped).map(([week, items]) => (

<section key={week}>

<h2>Semaine {week}</h2>

{items.map(lesson => (

<LessonCard key={lesson.id} lesson={lesson} />

))}

</section>

))}

</div>

);

⚡ useMemo Ă©vite de recalculer le groupement Ă  chaque rendu — indispensable si la liste est grande.

Rendu Markdown en React

Le format standard pour les contenus pédagogiques

Source Markdown

# Les variables

En JS, on déclare avec

`let` ou `const`.

```js

const x = 42;

```

Rendu HTML

Les variables

En JS, on déclare avec let ou const.

const x = 42;

Installer et utiliser react-markdown

1ïžâƒŁ Installation

npm install react-markdown

2ïžâƒŁ Utilisation de base

import Markdown from 'react-markdown';

function LessonContent({ content }: { content: string }) {

return <Markdown>{content}</Markdown>;

}

✅ react-markdown convertit la string Markdown en Ă©lĂ©ments React — pas de dangerouslySetInnerHTML.

Styler le Markdown avec des composants

<Markdown

components={{

h1: ({ children }) => (

<h1 className="text-3xl font-bold text-indigo-600 mb-4">

{children}

</h1>

),

code: ({ children }) => (

<code className="bg-slate-100 px-2 py-1 rounded font-mono text-sm">

{children}

</code>

),

}}

>

{content}

</Markdown>

💡 Chaque balise HTML peut ĂȘtre remplacĂ©e par un composant React — contrĂŽle total du style.

⚠ SĂ©curitĂ© : le Markdown non sanitisĂ© est une faille XSS

Si le contenu Markdown vient d'un utilisateur, il peut contenir du HTML malveillant.

❌ Dangereux

// Du HTML brut dans le Markdown

<script>vol_cookies()</script>

<img src=x onerror="hack()">

✅ SĂ©curisĂ© avec rehype-sanitize

import rehypeSanitize from 'rehype-sanitize';

<Markdown

rehypePlugins={[rehypeSanitize]}

>{content}</Markdown>

🔒 Rùgle : si le contenu vient d'une API ou d'un utilisateur, toujours utiliser rehype-sanitize.

Navigation : Breadcrumbs

Aider l'utilisateur à se repérer dans l'arborescence

Accueil / Leçons / Les Variables

L'utilisateur sait exactement oĂč il est, et peut naviguer en un clic.

Implémenter les breadcrumbs

interface BreadcrumbItem {

label: string;

to?: string;

}

function Breadcrumb({ items }: { items: BreadcrumbItem[] }) {

return (

<nav className="flex items-center gap-2 text-sm">

{items.map((item, i) => (

<span key={i} className="flex items-center gap-2">

{i > 0 && <span>/</span>}

{item.to ? <Link to={item.to}>{item.label}</Link>

: <span>{item.label}</span>}

</span>

))}

</nav>

);

}

Breadcrumbs dans la page Détail

function LessonDetail() {

const { id } = useParams();

const { data: lesson } = useQuery(...);

return (

<div>

<Breadcrumb items={[

{ label: 'Accueil', to: '/' },

{ label: 'Leçons', to: '/lessons' },

{ label: lesson?.title ?? '...' },

]} />

<h1>{lesson?.title}</h1>

<Markdown>{lesson?.content}</Markdown>

</div>

);

}

Filtrage & Recherche cÎté client

Réactif, immédiat, sans appel réseau

🔍

Recherche textuelle

Filtrer par titre ou contenu.

đŸ·ïž

Filtre par catégorie

Semaine, type, difficulté.

📭

État vide

"Aucune leçon trouvée."

Implémenter le filtrage

function LessonList() {

const [search, setSearch] = useState('');

const [week, setWeek] = useState<number | null>(null);

const { data: lessons } = useQuery(...);

const filtered = useMemo(() => {

return (lessons ?? []).filter(l => {

const matchSearch = l.title.toLowerCase()

.includes(search.toLowerCase());

const matchWeek = week === null || l.week === week;

return matchSearch && matchWeek;

});

}, [lessons, search, week]);

// ...

}

UI : barre de recherche + filtres

return (

<div>

<input

type="text"

placeholder="Rechercher une leçon..."

value={search}

onChange={e => setSearch(e.target.value)}

/>

{filtered.length === 0

? <p>Aucune leçon trouvée.</p>

: filtered.map(l => <LessonCard key={l.id} lesson={l} />)}

</div>

);

✅ L'Ă©tat vide est obligatoire — toujours prĂ©voir le cas "aucun rĂ©sultat".

CÎté client vs cÎté serveur

CĂŽtĂ© client ✅

OK pour < 1 000 éléments

✅ InstantanĂ©, pas de latence rĂ©seau

✅ Simple Ă  implĂ©menter

✅ Fonctionne hors ligne

❌ Toutes les donnĂ©es sont chargĂ©es

CĂŽtĂ© serveur 🚀

Nécessaire au-delà de 1 000 éléments

✅ Seules les donnĂ©es filtrĂ©es sont envoyĂ©es

✅ Scalable

❌ Un appel API par recherche

❌ Plus complexe (debounce, Ă©tats)

💡 Pour les leçons d'un bootcamp (< 200 Ă©lĂ©ments), le filtrage cĂŽtĂ© client est parfait.

PiÚges courants à éviter

❌ Dupliquer les donnĂ©es entre liste et dĂ©tail

→ Utiliser le cache React Query : queryKey: ['lessons', id] rĂ©utilise les donnĂ©es dĂ©jĂ  chargĂ©es.

❌ Oublier les Ă©tats vides et les erreurs

if (isLoading) return <Skeleton />;

if (isError) return <ErrorMessage />;

if (!lessons?.length) return <EmptyState />;

❌ Markdown non sanitisĂ© = faille XSS

→ Toujours ajouter rehypePlugins={[rehypeSanitize]} si le contenu vient d'une API ou d'un utilisateur.

Vue d'ensemble : architecture complĂšte

/lessons

React Query fetch → groupBy(week) → rendu des sections → barre de recherche + filtre semaine

/lessons/:id

useParams(id) → React Query fetch → Breadcrumb → <Markdown> + rehype-sanitize → bouton retour

React Router

Navigation & params

React Query

Cache & états

react-markdown

Contenu riche

Points clés à retenir

đŸ—ș Le pattern liste/dĂ©tail est le plus courant du web

URL paramĂ©trĂ©e /lessons/:id + useParams() + React Query — c'est la base.

📩 Grouper cĂŽtĂ© client est simple avec reduce()

Plus simple que de demander une API diffĂ©rente — et useMemo Ă©vite les recalculs.

📝 Markdown = format standard pour les contenus pĂ©dagogiques

react-markdown + rehype-sanitize pour un rendu sûr et stylé.

🔍 Filtrage cĂŽtĂ© client OK pour < 1 000 Ă©lĂ©ments

Au-delà, déléguer au serveur avec une query paramétrée.

À retenir !

useParams()

Récupérer l':id depuis l'URL

groupBy + reduce()

Tableau plat → structure hiĂ©rarchique

<Markdown>

Rendu sûr avec rehype-sanitize

Breadcrumb

Composant de navigation contextuelle

Toujours prévoir : loading · erreur · état vide

Exercices pratiques

1. Page liste de leçons

Implémenter LessonList : fetch via React Query, rendu des cartes, lien vers la page détail.

2. Grouper par semaine

Écrire la fonction groupBy() et l'utiliser pour afficher les leçons section par section.

3. Page détail avec Markdown

Afficher le contenu d'une leçon avec react-markdown + breadcrumbs + bouton retour.

4. Recherche et filtres

Ajouter une barre de recherche par titre et un filtre par semaine avec état vide.

Questions ?

Liste · Détail · Markdown · Breadcrumbs · Filtrage

Le pattern liste/détail est partout.

MaĂźtrisez-le une fois, et vous pouvez construire n'importe quelle application de contenu.