02Oordeel5 min

Wat er altijd gebeurt als ik iets met de hand begin

Ik heb vier keer met de hand een editor gebouwd. Drie ervan zijn herschreven met een library. De vierde haalde de twee dagen niet.

Door Salva Castells

Omslagafbeelding: Globetrotter19, CC BY-SA 4.0.
In dit bericht

In september 2024 was ik drie weken bezig met een blokkeneditor. Het idee was het gebruikelijke: elke alinea is een blok, je sleept het, je herschikt het, en elk blok heeft zijn eigen AI-knop om het te herschrijven. Niets exotisch. Notion doet het al jaren.

Ik bouwde het met dnd-kit voor het slepen en één editorinstantie per blok. De component Block kreeg id, index, content, onDelete, onContentChange, een functie regenerateContent met vijf parameters, de hele lijst blokken voor de context, en een AbortController in een ref om het genereren halverwege af te breken. Het werkte. Een van de commits van die week heet letterlijk "never give up!!". Dat is wat er is blijven staan.

Zes weken later was dat allemaal verwijderd en stond er in plaats daarvan één enkele editor met extensies.

Wat er stukgaat als jij de blokken orkestreert

Het probleem was niet het slepen. Het slepen ging goed. Het probleem was al het andere, en ik ontdekte het in deze volgorde:

  • Content naar binnen synchroniseren. Elk blok had een useEffect die content !== editor.getHTML() vergeleek om setContent aan te roepen. HTML als string vergelijken om te bepalen of de state is veranderd, is precies het soort ding dat lijkt te werken totdat het dat niet doet.
  • Selectie over blokken heen. Bestaat niet. Als elk blok zijn eigen editor is, kan de gebruiker niet selecteren vanaf het midden van een alinea tot het midden van de volgende. Er is geen halve oplossing: of er is één document, of er zijn er N.
  • Ongedaan maken. Elke instantie heeft zijn eigen historie-stack. Ctrl+Z maakt iets ongedaan in het blok waar de cursor staat, niet het laatste wat je hebt gedaan. Dat verwacht niemand.
  • Plakken. Je plakt drie alinea's in een blok en die drie blijven binnen één blok zitten. Ze opsplitsen is met de hand herbouwen wat het documentschema al kan.

Geen van die vier dingen zat in mijn hoofd toen ik begon. Alle vier doken op in precies de volgorde waarin ik ze heb opgeschreven.

De herschrijving was korter dan het prototype

Half oktober stond de map block/ al naast een map block2/ met drie bestanden: de editor, de werkbalk en een hook. De volledige hook was veertig regels: StarterKit, TextStyle, FontFamily, TextAlign, Highlight, Placeholder. Zes extensies gedeclareerd in een array, en daarmee kreeg je de verenigde historie, de doorlopende selectie en het plakken met schema erbij. In november kwamen de takenlijsten, de codeblokken en de afbeeldingen via URL: per stuk één import en één regel in de array.

De naam van de map zegt alles. block2 is geen refactor. Het is erkennen dat wat ernaast staat niet te redden valt.

Hetzelfde met de CSS van deze site. Ik had een bestand van duizend zeshonderdeenentachtig regels met eigen classes. Bij het omzetten naar componenten overleefden alleen de kleurtokens: honderddertien regels in globals.css. De rest was herbouw.

Waar de library altijd wint

Het is niet "gebruik libraries". Het is dat er concrete categorieën zijn waar het probleem groter is dan het van buitenaf lijkt, en dat zijn altijd dezelfde:

  • Rich text-editors. Selectie, historie, plakken en schema zijn één systeem. Je kunt er niet drie van de vier uitkiezen.
  • Authenticatie en sessies. Niemand bedankt je voor de refresh token die je zelf hebt geschreven.
  • Datums en tijdzones. Het rare geval komt bij je in productie, niet op je eigen machine.
  • Stijlen. Een eigen classes-systeem begint schoner en eindigt als een slechter gedocumenteerd framework.

Het gemeenschappelijke signaal is dit: als het moeilijke deel niet het eerste scherm is maar de interactie tussen onderdelen die je bij het begin niet ziet, wint de library. Als het moeilijke deel je businesslogica is, is er geen library.

Hoe ik het vandaag beslis

Ik heb de volgorde omgedraaid. Nu begin ik met de library en stap ik daar alleen uit als die me blokkeert.

Een collaboratieve editor die ik afgelopen zomer nodig had, loste ik zo op: starten vanaf een PlateJS-template en Yjs erbovenop zetten. Drie dagen na de eerste commit stonden er al remote cursors van andere gebruikers op het scherm. Maanden eerder was ik begonnen met de Block met vijf props. Voor een notitietool in Markdown vorig jaar heb ik niet eens over de editor nagedacht: CodeMirror, een import van highlight.js in de CSS, en verder met mijn eigen werk.

En er is een geval dat nog sneller gaat. In april 2025 schreef ik een agent met tools, een agent.ts met zijn contexttools. De dag erna staat er een commit die dat verwijdert en direct de streaming van de provider gebruikt. Minder dan vierentwintig uur tussen bouwen en weggooien, en dat is de goede uitkomst.

De echte kosten

Het met de hand bouwen is geen weggegooide tijd: ik weet wat plakken doet in een editor omdat ik het verkeerd heb herbouwd. Die kennis koop je niet door documentatie te lezen.

De kosten zitten in wanneer je het ontdekt. Drie weken handgemaakte blokken en zes weken samenleven tussen block/ en block2/ zijn twee maanden. De eerste versie van een 3D-kaart hield het tien dagen vol voordat een branch met de naam map-v2 hem helemaal verving; daar waren de kosten tien dagen, geen twee maanden, omdat ik eerder afkapte.

Het verschil tussen die twee is niet hoeveel ik wist toen ik begon. Het is hoe lang ik erover deed om te accepteren dat ik al genoeg wist.

Het met de hand bouwen is een dure manier om de documentatie te lezen. Soms is het dat waard; wat niet de moeite waard is, is er twee keer voor betalen voor hetzelfde.