02Judgement5 min

What always ends up happening when I build something by hand

I've built editors by hand four times. Three ended up rewritten with a library. The fourth didn't make it past two days.

By Salva Castells

Cover image: Globetrotter19, CC BY-SA 4.0.
In this post

In September 2024 I spent three weeks building a block editor. The usual idea: every paragraph is a block, you drag it, you reorder it, and each block has its own AI button to rewrite it. Nothing exotic. Notion has been doing it for years.

I built it with dnd-kit for dragging and one editor instance per block. The Block component took id, index, content, onDelete, onContentChange, a regenerateContent function with five parameters, the whole block list for context, and an AbortController in a ref to cut generation halfway. It worked. One of that week's commits is literally called "never give up!!". That's what ended up in the log.

Six weeks later all of it was deleted and in its place there was a single editor with extensions.

What breaks when you orchestrate the blocks yourself

The problem wasn't the dragging. The dragging came out fine. The problem was everything else, and I found it in this order:

  • Syncing content inwards. Each block had a useEffect comparing content !== editor.getHTML() to call setContent. Comparing HTML as a string to decide whether state changed is exactly the kind of thing that looks like it works until it doesn't.
  • Selection across blocks. Doesn't exist. If each block is its own editor, the user can't select from the middle of one paragraph to the middle of the next. There's no partial fix: either there's one document or there are N documents.
  • Undo. Each instance has its own history stack. Ctrl+Z undoes in the block where the cursor is, not the last thing you did. Nobody expects that.
  • Paste. You paste three paragraphs into a block and all three stay inside one block. Splitting them is hand-reimplementing what the document schema already knows how to do.

None of those four things were in my head when I started. All four showed up in the exact order I wrote them.

The rewrite was shorter than the prototype

By mid-October the block/ folder was already living next to a block2/ folder with three files: the editor, the toolbar and a hook. The whole hook was forty lines: StarterKit, TextStyle, FontFamily, TextAlign, Highlight, Placeholder. Six extensions declared in an array, and with that came unified history, continuous selection and schema-aware paste. In November came task lists, code blocks and images by URL: one import and one array entry each.

The folder name says it all. block2 isn't a refactor. It's admitting the thing next to it can't be saved.

Same with this site's CSS. I had a one-thousand-six-hundred-and-eighty-one line file of my own classes. Moving it to components, the only thing that survived were the color tokens: one hundred and thirteen lines in globals.css. The rest was reimplementation.

Where the library always wins

It isn't "use libraries". It's that there are specific categories where the problem is bigger than it looks from outside, and they're always the same ones:

  • Rich text editors. Selection, history, paste and schema are one system. You can't take three out of four.
  • Auth and sessions. Nobody thanks you for the refresh token you wrote yourself.
  • Dates and time zones. The weird case reaches you in production, not on your machine.
  • Styles. Your own class system starts out cleaner and ends up a worse-documented framework.

The common signal is this: if the hard part isn't the first screen but the interaction between pieces you can't see at the start, the library wins. If the hard part is your business logic, there is no library.

How I decide today

I flipped the order. Now I start with the library and only leave it if it blocks me.

A collaborative editor I needed last summer went like this: start from a PlateJS template and put Yjs on top. Three days after the first commit there were already remote cursors from other users on screen. Months earlier I'd have started with the Block with five props. For a Markdown notes tool last year I didn't even consider the editor: CodeMirror, a highlight.js import in the CSS, and on with my work.

And there's a case that goes even faster. In April 2025 I wrote an agent with tools, an agent.ts with its context tools. The next day there's a commit deleting it and using the provider's streaming directly. Less than twenty-four hours between building it and deleting it, and that's the good outcome.

The real cost

Building it by hand isn't wasted time: I know what paste does in an editor because I reimplemented it badly. You don't buy that knowledge by reading docs.

The cost is in when you find out. Three weeks of hand-rolled blocks and six more of block/ and block2/ coexisting is two months. The first version of a 3D map lasted ten days before a branch called map-v2 replaced it whole; there the cost was ten days, not two months, because I cut earlier.

The difference between the two isn't how much I knew at the start. It's how long I took to accept I already knew enough.

Building it by hand is an expensive way to read the documentation. Sometimes it's worth paying; what's never worth it is paying twice for the same thing.