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.
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.
Pattern liste / détail
Page liste avec filtres â page dĂ©tail avec contenu complet.
Grouper par semaine / jour
Transformer un tableau plat en structure hiérarchique avec reduce().
Rendu Markdown cÎté client
react-markdown â afficher, styler, sĂ©curiser.
Navigation : breadcrumbs & URL paramétrées
/lessons/:id, retour Ă la liste, fil d'Ariane.
Filtrage et recherche cÎté client
Recherche live, filtre par catégorie, état vide.
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.
// 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().
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>
);
}
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.
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
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.
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.
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.
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.
<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.
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.
Aider l'utilisateur à se repérer dans l'arborescence
L'utilisateur sait exactement oĂč il est, et peut naviguer en un clic.
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>
);
}
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>
);
}
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."
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]);
// ...
}
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 â
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.
â 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.
React Query fetch â groupBy(week) â rendu des sections â barre de recherche + filtre semaine
useParams(id) â React Query fetch â Breadcrumb â <Markdown> + rehype-sanitize â bouton retour
React Router
Navigation & params
React Query
Cache & états
react-markdown
Contenu riche
đșïž 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.
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
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.
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.