Comment intégrer un éditeur Markdown WYSIWYG dans une application ReactJS

Apprends à remplacer un textarea par un éditeur Markdown WYSIWYG MDX Editor dans ton projet React Router 7

5 min read
Déverrouillez votre potentiel

avec React Router 7

Vous en avez marre de...

❌ perdre du temps à chercher des informations éparpillées
❌ ne pas avoir de retour sur votre progression
Assistant IA spécialisé

Posez vos questions 24/7 à notre IA experte en React Router 7

Quiz interactifs

Validez vos acquis avec des quiz personnalisés et un feedback instantané

9 modules
72 leçons
Accès à vie
299.49
-35%

Mise en place d’un éditeur Markdown WYSIWYG

Dans cette leçon, on remplace le simple <textarea> du back-office par un éditeur Markdown visuel grâce à MDX Editor. Objectifs :

  1. Installer la librairie et son CSS.
  2. Créer un composant <MarkdownField> compatible Conform.
  3. L’utiliser dans le formulaire d’ajout / d’édition de produit.
  4. Gérer le SSR grâce à <ClientOnly> et afficher un squelette pendant le chargement.
  5. Ajouter un bouton « YouTube » (directive) pour insérer une vidéo.

1 Installer MDX Editor et ses dépendances

install.sh
1
npm install @mdxeditor/editor

Pourquoi ? La dépendance packagée contient :

  • le core de l’éditeur ;
  • les plugins (liste, tableaux, images, diff…).

2 Importer le style global

Ajoute la feuille CSS de la librairie une seule fois dans root.tsx :

app/root.tsx
1
import "@mdxeditor/editor/style.css";

Cela évite de dupliquer l’import dans chaque page.


3 Créer le composant MarkdownField

Nous voulons :

  • un champ contrôlé pour synchroniser la valeur avec Conform ;
  • un fallback skeleton pour éviter le CLS ;
  • un wrapper <ClientOnly> car MDX Editor ne s’hydrate qu’au client.
app/components/markdown-field.tsx
1
'use client'
2
import {
3
MDXEditor,
4
headingsPlugin,
5
listsPlugin,
6
quotePlugin,
7
markdownShortcutPlugin,
8
tablePlugin,
9
linkPlugin,
10
linkDialogPlugin,
11
imagePlugin,
12
diffSourcePlugin,
13
toolbarPlugin,
14
/* … */
15
} from "@mdxeditor/editor";
16
import { ClientOnly } from "remix-utils/client-only";
17
import { useId, useRef } from "react";
18
import { Label } from "~/components/ui/label";
19
import { ErrorList } from "~/components/forms";
20
21
export function MarkdownField({
22
content,
23
onContentChange,
24
textareaProps,
25
labelProps,
26
errors,
27
}: {
28
content: string;
29
onContentChange: (value: string) => void;
30
textareaProps: React.TextareaHTMLAttributes<HTMLTextAreaElement>;
31
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>;
32
errors?: Array<string>;
33
}) {
34
const id = textareaProps.id ?? textareaProps.name ?? useId();
35
const errorId = errors?.length ? `${id}-error` : undefined;
36
37
return (
38
<div className="space-y-2">
39
<Label htmlFor={id} {...labelProps} />
40
41
{/* champ caché pour Conform */}
42
<textarea {...textareaProps} value={content} className="hidden" readOnly />
43
44
<ClientOnly
45
fallback={
46
<div className="rounded-md border bg-muted/30 p-4">
47
<p className="text-sm text-muted-foreground">Chargement de l’éditeur…</p>
48
</div>
49
}
50
>
51
{() => (
52
<MDXEditor
53
markdown={content}
54
onChange={onContentChange}
55
contentEditableClassName="prose max-w-none min-h-[200px] p-4 border rounded-md focus:outline-none"
56
plugins={[
57
headingsPlugin(),
58
listsPlugin(),
59
quotePlugin(),
60
markdownShortcutPlugin(),
61
linkPlugin(),
62
linkDialogPlugin(),
63
tablePlugin(),
64
imagePlugin(),
65
diffSourcePlugin({ viewMode: "rich-text" }),
66
toolbarPlugin(),
67
]}
68
/>
69
)}
70
</ClientOnly>
71
72
<ErrorList id={errorId} errors={errors} />
73
</div>
74
);
75
}

Pourquoi ces choix ?

  • textarea hidden : Conform récupère toujours la valeur côté serveur.
  • fallback : évite un flash blanc pendant l’hydratation.
  • plugins : package de base (titres, listes, images, tableaux, diff).

4 Remplacer <TextareaField> par <MarkdownField>

Dans la route admin d’édition de produit :

tsx {38-52} app/routes/admin+/products.$productSlug.tsx
1
- <Field
2
- labelProps={{ children: "Contenu détaillé (Markdown)" }}
3
- textareaProps={getTextareaProps(fields.content)}
4
- errors={fields.content.errors}
5
- />
6
+ <MarkdownField
7
+ content={content}
8
+ onContentChange={setContent}
9
+ labelProps={{ children: "Contenu détaillé (Markdown)" }}
10
+ textareaProps={getTextareaProps(fields.content)}
11
+ errors={fields.content.errors}
12
+ />

Explications :

  • content = state local React.
  • setContent met à jour le champ caché → Conform le reçoit côté serveur.

5 Ajouter un bouton « YouTube » (directive)

  1. Crée un descriptor pour transformer ::youtube{id="VIDEO_ID"} en iframe.
  2. Expose un bouton dans la toolbar qui insère la directive.
{15-34} app/components/markdown-field.tsx
1
import { Youtube } from "lucide-react";
2
import {
3
directivesPlugin,
4
type DirectiveDescriptor,
5
insertDirective$,
6
usePublisher,
7
DialogButton,
8
} from "@mdxeditor/editor";
9
10
/* descriptor */
11
const youtubeDescriptor: DirectiveDescriptor = {
12
name: "youtube",
13
attributes: ["id"],
14
hasChildren: false,
15
testNode: (n) => n.name === "youtube",
16
Editor: ({ mdastNode }) => (
17
<iframe
18
className="my-4 aspect-video w-full rounded-md"
19
src={`https://www.youtube.com/embed/${mdastNode.attributes?.id ?? ""}`}
20
loading="lazy"
21
/>
22
),
23
};
24
25
/* bouton toolbar */
26
const YouTubeButton = () => {
27
const insert = usePublisher(insertDirective$);
28
return (
29
<DialogButton
30
tooltipTitle="Insérer une vidéo YouTube"
31
dialogInputPlaceholder="URL YouTube"
32
buttonContent={<Youtube className="h-4 w-4" />}
33
onSubmit={(url) => {
34
const id = new URL(url).searchParams.get("v");
35
id && insert({ name: "youtube", type: "leafDirective", attributes: { id } });
36
}}
37
/>
38
);
39
};

Ajoute le plugin :

{18} plugins={[..., directivesPlugin({ directiveDescriptors: [youtubeDescriptor] })]}
1

Et place le bouton dans toolbarContents.


6 Rendu côté public

Le Markdown côté vitrine doit comprendre la directive youtube.

app/components/markdown.tsx
1
import remarkDirective from "remark-directive";
2
import { visit } from "unist-util-visit";
3
4
/* plugin qui remplace la directive youtube par un iframe */
5
function remarkYouTube() {
6
return (tree) => {
7
visit(tree, (node) => {
8
if (node.type === "leafDirective" && node.name === "youtube") {
9
node.data = {
10
hName: "iframe",
11
hProperties: {
12
src: `https://www.youtube.com/embed/${node.attributes.id}`,
13
className: "my-6 w-full aspect-video rounded-md",
14
loading: "lazy",
15
},
16
};
17
}
18
});
19
};
20
}

Puis :

app/components/markdown.tsx
1
<Markdown remarkPlugins={[remarkGfm, remarkDirective, remarkYouTube]}>
2
{content}
3
</Markdown>

Points d’attention

  1. SSR : MDX Editor ne fonctionne que côté client → toujours l’envelopper dans <ClientOnly>.
  2. Validation : la valeur Markdown est stockée dans un <textarea hidden> pour que Conform l’envoie dans le FormData.
  3. Performance : l’import CSS global empêche les FOUC ; le skeleton évite les CLS.
  4. Sécurité : si tu autorises la directive YouTube, assure-toi de filtrer d’autres iframes potentiellement dangereuses.

Commit de la leçon

Terminal
1
git add .
2
git commit -m "feat(admin): éditeur Markdown WYSIWYG MDX Editor + directive YouTube"

Tu disposes maintenant d’un éditeur riche, type-safe et extensible pour tes contenus produits. Prochaine étape : gestion des images et upload vers un CDN !

Premium
Quiz interactif
Testez vos connaissances et validez votre compréhension du module avec notre quiz interactif.
1

Comprendre les concepts fondamentaux

Quelle est la principale différence entre les composants client et serveur dans React ?

Les composants client s'exécutent uniquement dans le navigateur
Les composants serveur peuvent utiliser useState
Les composants client sont plus rapides
Il n'y a aucune différence significative
2

Optimisation des performances

Quelle technique est recommandée pour éviter les rendus inutiles dans React ?

Utiliser React.memo pour les composants fonctionnels
Ajouter plus d'états locaux
Éviter d'utiliser les props
Toujours utiliser les class components
3

Architecture des données

Quel hook permet de gérer les effets de bord dans un composant React ?

useEffect
useState
useMemo
useContext
4

Gestion des erreurs

Comment implémenter la gestion des erreurs pour les requêtes API dans React ?

Utiliser try/catch avec async/await
Ignorer les erreurs
Toujours afficher un message d'erreur
Rediriger l'utilisateur
5

Déploiement et CI/CD

Quelle est la meilleure pratique pour déployer une application React en production ?

Utiliser un service CI/CD comme GitHub Actions
Copier les fichiers manuellement via FTP
Envoyer le code source complet
Ne jamais mettre à jour l'application

Débloquez ce quiz et tous les autres contenus premium en achetant ce cours