02Jugement5 min

Ce qui finit toujours par arriver quand je pars de zéro

J'ai construit des éditeurs à la main quatre fois. Trois ont fini réécrits avec une bibliothèque. Le quatrième n'a pas tenu deux jours.

Par Salva Castells

Image de couverture : Globetrotter19, CC BY-SA 4.0.
Dans cet article

En septembre 2024, j'ai passé trois semaines à construire un éditeur par blocs. L'idée était la même que d'habitude : chaque paragraphe est un bloc, on le glisse, on le réordonne, et chaque bloc a son propre bouton d'IA pour le réécrire. Rien d'exotique. Notion le fait depuis des années.

Je l'ai monté avec dnd-kit pour le glisser-déposer et une instance d'éditeur par bloc. Le composant Block recevait id, index, content, onDelete, onContentChange, une fonction regenerateContent à cinq paramètres, la liste entière des blocs pour le contexte, et un AbortController dans une ref pour couper la génération en cours. Ça marchait. Un des commits de cette semaine-là s'appelle littéralement "never give up!!". C'est ce qui est resté écrit.

Six semaines plus tard, tout cela était supprimé et, à la place, il y avait un seul éditeur avec des extensions.

Ce qui casse quand c'est toi qui orchestres les blocs

Le problème, ce n'était pas le glisser-déposer. Le glisser-déposer a bien marché. Le problème, c'était tout le reste, et je l'ai découvert dans cet ordre :

  • Synchroniser le contenu vers l'intérieur. Chaque bloc avait un useEffect qui comparait content !== editor.getHTML() pour appeler setContent. Comparer du HTML comme une chaîne pour décider si l'état a changé, c'est exactement le genre de chose qui a l'air de marcher jusqu'au jour où non.
  • Sélection entre blocs. Elle n'existe pas. Si chaque bloc est son propre éditeur, l'utilisateur ne peut pas sélectionner du milieu d'un paragraphe au milieu du suivant. Pas de correctif partiel : soit il y a un document, soit il y en a N.
  • Annuler. Chaque instance a sa propre pile d'historique. Ctrl+Z annule dans le bloc où se trouve le curseur, pas la dernière chose que tu as faite. Personne ne s'attend à ça.
  • Coller. Tu colles trois paragraphes dans un bloc et les trois restent dans un seul bloc. Les découper, c'est réimplémenter à la main ce que le schéma du document sait déjà faire.

Aucune de ces quatre choses n'était dans ma tête au départ. Les quatre sont apparues dans l'ordre exact où je viens de les écrire.

La réécriture a été plus courte que le prototype

À la mi-octobre, le dossier block/ cohabitait déjà avec un dossier block2/ qui contenait trois fichiers : l'éditeur, la barre d'outils et un hook. Le hook complet faisait quarante lignes : StarterKit, TextStyle, FontFamily, TextAlign, Highlight, Placeholder. Six extensions déclarées dans un tableau, et avec ça venaient l'historique unifié, la sélection continue et le collage avec schéma. En novembre sont arrivées les listes de tâches, les blocs de code et les images par URL : un import et une entrée dans le tableau pour chacune.

Le nom du dossier dit tout. block2 n'est pas un refactor. C'est reconnaître que ce qu'il y a à côté est irrécupérable.

Pareil avec le CSS de ce site. J'avais un fichier de mille six cent quatre-vingt-une lignes avec mes propres classes. En passant aux composants, la seule chose qui a survécu, ce sont les tokens de couleur : cent treize lignes dans globals.css. Le reste, c'était de la réimplémentation.

Là où la bibliothèque gagne toujours

Ce n'est pas « utilise des bibliothèques ». C'est qu'il y a des catégories précises où le problème est plus gros qu'il n'en a l'air de l'extérieur, et ce sont toujours les mêmes :

  • Éditeurs de texte enrichi. Sélection, historique, collage et schéma forment un seul système. On ne peut pas en prendre trois sur quatre.
  • Authentification et sessions. Personne ne te remerciera pour le refresh token que tu as écrit toi-même.
  • Dates et fuseaux horaires. Le cas tordu t'arrive en production, pas sur ta machine.
  • Styles. Un système de classes maison commence plus propre et finit en framework moins bien documenté.

Le signal commun est celui-ci : si la partie difficile n'est pas le premier écran mais l'interaction entre des pièces que tu ne vois pas au départ, la bibliothèque gagne. Si la partie difficile, c'est ta logique métier, il n'y en a pas.

Comment je décide aujourd'hui

J'ai changé l'ordre. Maintenant je démarre avec la bibliothèque et je n'en sors que si elle me bloque.

Un éditeur collaboratif dont j'avais besoin l'été dernier s'est réglé comme ça : partir d'un gabarit PlateJS et ajouter Yjs par-dessus. Trois jours après le premier commit, il y avait déjà à l'écran les curseurs distants des autres utilisateurs. Quelques mois plus tôt, j'aurais commencé par le Block à cinq props. Pour un outil de notes en Markdown l'an dernier, je ne me suis même pas posé la question de l'éditeur : CodeMirror, un import de highlight.js dans le CSS, et au travail.

Et il y a un cas qui va encore plus vite. En avril 2025, j'ai écrit un agent avec des outils, un agent.ts avec ses tools de contexte. Le lendemain, il y a un commit qui le supprime et utilise directement le streaming du fournisseur. Moins de vingt-quatre heures entre le construire et l'effacer, et c'est le bon résultat.

Le coût réel

Le construire à la main, ce n'est pas du temps jeté : je sais ce que fait le collage dans un éditeur parce que je l'ai mal réimplémenté. Ce savoir-là ne s'achète pas en lisant la documentation.

Le coût est dans le moment où tu le découvres. Trois semaines de blocs à la main et six de plus de cohabitation entre block/ et block2/, ça fait deux mois. La première version d'une carte en 3D a duré dix jours avant qu'une branche appelée map-v2 la remplace entièrement ; là, le coût a été de dix jours, pas de deux mois, parce que j'ai coupé plus tôt.

La différence entre les deux, ce n'est pas ce que je savais au départ. C'est le temps que j'ai mis à accepter que j'en savais déjà assez.

Le construire à la main est une façon coûteuse de lire la documentation. Parfois ça vaut le coup de payer ; ce qui n'en vaut pas la peine, c'est de payer deux fois pour la même chose.