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
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 :
- Installer la librairie et son CSS.
- Créer un composant
<MarkdownField>compatible Conform. - L’utiliser dans le formulaire d’ajout / d’édition de produit.
- Gérer le SSR grâce à
<ClientOnly>et afficher un squelette pendant le chargement. - Ajouter un bouton « YouTube » (directive) pour insérer une vidéo.
1 Installer MDX Editor et ses dépendances
1npm 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 :
1import "@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.
1'use client'2import {3MDXEditor,4headingsPlugin,5listsPlugin,6quotePlugin,7markdownShortcutPlugin,8tablePlugin,9linkPlugin,10linkDialogPlugin,11imagePlugin,12diffSourcePlugin,13toolbarPlugin,14/* … */15} from "@mdxeditor/editor";16import { ClientOnly } from "remix-utils/client-only";17import { useId, useRef } from "react";18import { Label } from "~/components/ui/label";19import { ErrorList } from "~/components/forms";2021export function MarkdownField({22content,23onContentChange,24textareaProps,25labelProps,26errors,27}: {28content: string;29onContentChange: (value: string) => void;30textareaProps: React.TextareaHTMLAttributes<HTMLTextAreaElement>;31labelProps: React.LabelHTMLAttributes<HTMLLabelElement>;32errors?: Array<string>;33}) {34const id = textareaProps.id ?? textareaProps.name ?? useId();35const errorId = errors?.length ? `${id}-error` : undefined;3637return (38<div className="space-y-2">39<Label htmlFor={id} {...labelProps} />4041{/* champ caché pour Conform */}42<textarea {...textareaProps} value={content} className="hidden" readOnly />4344<ClientOnly45fallback={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<MDXEditor53markdown={content}54onChange={onContentChange}55contentEditableClassName="prose max-w-none min-h-[200px] p-4 border rounded-md focus:outline-none"56plugins={[57headingsPlugin(),58listsPlugin(),59quotePlugin(),60markdownShortcutPlugin(),61linkPlugin(),62linkDialogPlugin(),63tablePlugin(),64imagePlugin(),65diffSourcePlugin({ viewMode: "rich-text" }),66toolbarPlugin(),67]}68/>69)}70</ClientOnly>7172<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 :
1- <Field2- labelProps={{ children: "Contenu détaillé (Markdown)" }}3- textareaProps={getTextareaProps(fields.content)}4- errors={fields.content.errors}5- />6+ <MarkdownField7+ 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.setContentmet à jour le champ caché → Conform le reçoit côté serveur.
5 Ajouter un bouton « YouTube » (directive)
- Crée un descriptor pour transformer
::youtube{id="VIDEO_ID"}en iframe. - Expose un bouton dans la toolbar qui insère la directive.
1import { Youtube } from "lucide-react";2import {3directivesPlugin,4type DirectiveDescriptor,5insertDirective$,6usePublisher,7DialogButton,8} from "@mdxeditor/editor";910/* descriptor */11const youtubeDescriptor: DirectiveDescriptor = {12name: "youtube",13attributes: ["id"],14hasChildren: false,15testNode: (n) => n.name === "youtube",16Editor: ({ mdastNode }) => (17<iframe18className="my-4 aspect-video w-full rounded-md"19src={`https://www.youtube.com/embed/${mdastNode.attributes?.id ?? ""}`}20loading="lazy"21/>22),23};2425/* bouton toolbar */26const YouTubeButton = () => {27const insert = usePublisher(insertDirective$);28return (29<DialogButton30tooltipTitle="Insérer une vidéo YouTube"31dialogInputPlaceholder="URL YouTube"32buttonContent={<Youtube className="h-4 w-4" />}33onSubmit={(url) => {34const id = new URL(url).searchParams.get("v");35id && insert({ name: "youtube", type: "leafDirective", attributes: { id } });36}}37/>38);39};
Ajoute le plugin :
1
Et place le bouton dans toolbarContents.
6 Rendu côté public
Le Markdown côté vitrine doit comprendre la directive youtube.
1import remarkDirective from "remark-directive";2import { visit } from "unist-util-visit";34/* plugin qui remplace la directive youtube par un iframe */5function remarkYouTube() {6return (tree) => {7visit(tree, (node) => {8if (node.type === "leafDirective" && node.name === "youtube") {9node.data = {10hName: "iframe",11hProperties: {12src: `https://www.youtube.com/embed/${node.attributes.id}`,13className: "my-6 w-full aspect-video rounded-md",14loading: "lazy",15},16};17}18});19};20}
Puis :
1<Markdown remarkPlugins={[remarkGfm, remarkDirective, remarkYouTube]}>2{content}3</Markdown>
Points d’attention
- SSR : MDX Editor ne fonctionne que côté client → toujours l’envelopper dans
<ClientOnly>. - Validation : la valeur Markdown est stockée dans un
<textarea hidden>pour que Conform l’envoie dans leFormData. - Performance : l’import CSS global empêche les FOUC ; le skeleton évite les CLS.
- Sécurité : si tu autorises la directive YouTube, assure-toi de filtrer d’autres iframes potentiellement dangereuses.
Commit de la leçon
1git add .2git 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 !
Comprendre les concepts fondamentaux
Quelle est la principale différence entre les composants client et serveur dans React ?
Optimisation des performances
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Architecture des données
Quel hook permet de gérer les effets de bord dans un composant React ?