Apprends à remplacer un textarea par un éditeur Markdown WYSIWYG MDX Editor dans ton projet React Router 7
avec React Router 7
Posez vos questions 24/7 à notre IA experte en React Router 7
Validez vos acquis avec des quiz personnalisés et un feedback instantané
Dans cette leçon, on remplace le simple <textarea>
du back-office par un éditeur Markdown visuel grâce à MDX Editor.
Objectifs :
<MarkdownField>
compatible Conform.<ClientOnly>
et afficher un squelette pendant le chargement.1npm install @mdxeditor/editor
Pourquoi ? La dépendance packagée contient :
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.
MarkdownField
Nous voulons :
<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 ?
<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.setContent
met à jour le champ caché → Conform le reçoit côté serveur.::youtube{id="VIDEO_ID"}
en iframe.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
.
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>
<ClientOnly>
.<textarea hidden>
pour que Conform l’envoie dans le FormData
.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 !
Quelle est la principale différence entre les composants client et serveur dans React ?
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Quel hook permet de gérer les effets de bord dans un composant React ?
Comment implémenter la gestion des erreurs pour les requêtes API dans React ?
Quelle est la meilleure pratique pour déployer une application React en production ?