We wrześniu 2024 spędziłem trzy tygodnie na budowaniu edytora bloków. Pomysł jak zwykle ten sam: każdy akapit to blok, przeciągasz go, zmieniasz kolejność, a każdy blok ma własny przycisk AI do przepisania treści. Nic egzotycznego. Notion robi to od lat.
Złożyłem to z dnd-kit do przeciągania i jedną instancją edytora na blok. Komponent Block dostawał id, index, content, onDelete, onContentChange, funkcję regenerateContent z pięcioma parametrami, całą listę bloków dla kontekstu oraz AbortController w refie, żeby przerwać generowanie w połowie. Działało. Jeden z commitów z tamtego tygodnia nazywa się dosłownie "never give up!!". Tyle zostało na piśmie.
Sześć tygodni później to wszystko było skasowane, a na jego miejscu stał jeden edytor z rozszerzeniami.
Co się psuje, kiedy to ty orkiestrujesz bloki
Problemem nie było przeciąganie. Przeciąganie wyszło dobrze. Problemem była cała reszta i odkrywałem ją w tej kolejności:
- Synchronizacja treści do środka. Każdy blok miał
useEffectporównującycontent !== editor.getHTML(), żeby wywołaćsetContent. Porównywanie HTML-a jako ciągu znaków, żeby stwierdzić, czy stan się zmienił, to dokładnie ten rodzaj rzeczy, które wyglądają, jakby działały, dopóki nie przestaną. - Zaznaczanie między blokami. Nie istnieje. Jeśli każdy blok jest własnym edytorem, użytkownik nie może zaznaczyć tekstu od połowy jednego akapitu do połowy następnego. Nie ma częściowej naprawy: albo jest jeden dokument, albo jest N dokumentów.
- Cofanie. Każda instancja ma własny stos historii. Ctrl+Z cofa w bloku, w którym stoi kursor, a nie ostatnią rzecz, którą zrobiłeś. Nikt tego nie oczekuje.
- Wklejanie. Wklejasz trzy akapity w jeden blok i wszystkie trzy zostają w tym jednym bloku. Rozbicie ich to ręczne implementowanie od nowa tego, co schemat dokumentu już potrafi.
Żadnej z tych czterech rzeczy nie miałem w głowie, kiedy zaczynałem. Wszystkie cztery pojawiły się dokładnie w tej kolejności, w jakiej je tu wypisałem.
Przepisanie było krótsze niż prototyp
W połowie października katalog block/ sąsiadował już z katalogiem block2/, który miał trzy pliki: edytor, pasek narzędzi i hook. Cały hook to czterdzieści linii: StarterKit, TextStyle, FontFamily, TextAlign, Highlight, Placeholder. Sześć rozszerzeń zadeklarowanych w tablicy, a razem z nimi przyszła wspólna historia, ciągłe zaznaczanie i wklejanie zgodne ze schematem. W listopadzie doszły listy zadań, bloki kodu i obrazy z URL-a: jeden import i jeden wpis w tablicy na każdą rzecz.
Nazwa katalogu mówi wszystko. block2 to nie refaktor. To przyznanie, że tego obok już się nie uratuje.
Tak samo było z CSS-em tej strony. Miałem plik na tysiąc sześćset osiemdziesiąt jeden linii z własnymi klasami. Przy przejściu na komponenty przetrwały tylko tokeny kolorów: sto trzynaście linii w globals.css. Reszta była implementowaniem od nowa.
Gdzie biblioteka wygrywa zawsze
To nie jest "używaj bibliotek". Chodzi o to, że są konkretne kategorie, w których problem jest większy, niż wygląda z zewnątrz, i zawsze są to te same:
- Edytory tekstu sformatowanego. Zaznaczanie, historia, wklejanie i schemat to jeden system. Nie da się wziąć trzech z czterech.
- Uwierzytelnianie i sesje. Nikt ci nie podziękuje za refresh token, który napisałeś sam.
- Daty i strefy czasowe. Dziwny przypadek trafi cię na produkcji, nie na twojej maszynie.
- Style. Własny system klas zaczyna jako czystszy, a kończy jako gorzej udokumentowany framework.
Wspólny sygnał jest taki: jeśli trudną częścią nie jest pierwszy ekran, tylko interakcja między elementami, których na starcie nie widzisz, wygrywa biblioteka. Jeśli trudną częścią jest twoja logika biznesowa, biblioteki nie ma.
Jak decyduję dzisiaj
Zmieniłem kolejność. Teraz startuję od biblioteki i wychodzę z niej tylko wtedy, gdy mnie blokuje.
Edytor kolaboracyjny, którego potrzebowałem zeszłego lata, rozwiązałem tak: wyjść od szablonu PlateJS i dołożyć Yjs na wierzch. Trzy dni po pierwszym commicie na ekranie były już zdalne kursory innych użytkowników. Kilka miesięcy wcześniej zacząłbym od Block z pięcioma propsami. Przy narzędziu do notatek w Markdownie rok temu nawet nie rozważałem pisania edytora: CodeMirror, import highlight.js w CSS-ie i do roboty.
Jest też przypadek, który idzie jeszcze szybciej. W kwietniu 2025 napisałem agenta z narzędziami, agent.ts ze swoimi toolami kontekstowymi. Następnego dnia jest commit, który go usuwa i używa streamingu dostawcy bezpośrednio. Mniej niż dwadzieścia cztery godziny między zbudowaniem a skasowaniem, i to jest dobry wynik.
Prawdziwy koszt
Zbudowanie tego ręcznie to nie jest zmarnowany czas: wiem, co robi wklejanie w edytorze, bo źle je zaimplementowałem od nowa. Tej wiedzy nie kupisz, czytając dokumentację.
Koszt leży w tym, kiedy to odkrywasz. Trzy tygodnie ręcznych bloków plus sześć kolejnych współistnienia block/ i block2/ to dwa miesiące. Pierwsza wersja mapy 3D przeżyła dziesięć dni, zanim gałąź o nazwie map-v2 zastąpiła ją w całości; tam kosztem było dziesięć dni, nie dwa miesiące, bo uciąłem wcześniej.
Różnica między tymi dwoma przypadkami nie polega na tym, ile wiedziałem na starcie. Polega na tym, ile czasu zajęło mi przyjęcie do wiadomości, że wiedziałem już wystarczająco.
Budowanie czegoś ręcznie to drogi sposób czytania dokumentacji. Czasem warto za to zapłacić; nie warto płacić dwa razy za to samo.
