We hebben Slash een nieuwe merknaam gegeven als onderdeel van onze Fondsenwerving serie B, en we wilden ons product samen met ons merk visueel vernieuwen. We realiseerden ons al snel dat dit, naast het updaten van onze gebruikersinterface, een uitstekende gelegenheid was om een groot deel van de technische achterstand weg te werken die we in de afgelopen drie jaar van zeer snelle groei hadden opgebouwd.
Dus, project Facelift werd geboren - een kans voor ons om de basis te leggen voor Slash's frontend voor onze groeiende onderneming en ons team. We wilden beslissingen nemen die de tand des tijds zouden doorstaan door te leren van de fouten uit ons verleden.
Deel 1: De technische schuld aanpakken
In dit gedeelte zal ik elk punt van 'schulden' bespreken. Later, in deel 2, ga ik dieper in op onze oplossingen voor elk punt.
Emotie: CSS in JS-runtime-knelpunten
Ons originele ontwerpsysteem, gebouwd op basis van emotie en heeft ons goed gediend. Het belangrijkste voordeel van een CSS in JS-bibliotheek was de co-locatie van de markup en styling van een component, waardoor iteratie extreem snel verliep. Naarmate onze klanten echter groeiden en steeds grotere hoeveelheden data hadden, begonnen we prestatieknelpunten te zien in emotion. CSS-in-JS vereist runtime-overhead en kan de belangrijkste event loop vertragen. Dit is merkbaar bij het virtualiseren van grote lijsten en snel scrollen. Wanneer componenten snel worden gemount en unmount, heeft het dynamisch berekenen van stijlen en het genereren van stylesheets zo'n grote impact op de browser dat deze niet meer met 60 fps kan renderen. Andere bedrijven hebben vergelijkbare prestatieknelpunten gevonden en afgestapt van oplossingen die afhankelijk zijn van runtime-overhead.
Op rekwisieten gebaseerde 'godscomponenten'
Het oorspronkelijke concept van "God maakt bezwaar" komt voort uit OOP, waar een object te veel verantwoordelijkheid alleen zou dragen. We merkten dat soortgelijke dingen gebeurden in onze React-componenten, die we "God-componenten" zijn gaan noemen.
Veel van onze originele React-componenten zijn geschreven met een op props gebaseerde interface, waarbij je data en callbacks opgeeft en één enkele component rendert. Op het eerste gezicht biedt dit een elegante interface: je hoeft alleen maar data en props op te geven en kunt erop vertrouwen dat de component alle markup en bedrijfslogica voor zijn rekening neemt.
In de praktijk worden deze componenten, naarmate de codebase groeit, uitgebreid om toepassingen te dekken die oorspronkelijk niet waren overwogen of bedoeld. Telkens wanneer een engineer een nieuwe configuratie nodig had, voegde hij props toe, waardoor het onderhoud en het gebruik na verloop van tijd moeilijker werden. Deze componenten werden uiteindelijk 'God-componenten' die zorgvuldig moesten worden onderhouden, met eenmalige props om bepaalde problemen op te lossen, of 'vluchtluiken' om extra markup in de component zelf te injecteren/vervangen (vergelijkbaar met een render prop-patroon). Ik zal later in de blog codevoorbeelden laten zien.
Inconsistent formulierbeheer
Onze oorspronkelijke oplossing voor formulierbeheer was simpelweg React-statusbeheer. Lokale status is gemakkelijk te gebruiken, maar mist standaardisatie en diepgaande typeveiligheid, wat leidde tot inconsistente codekwaliteit. De manier waarop elke engineer lokale status implementeerde, verschilde enigszins: sommigen maakten gebruik van de context-API met één grote gedeelde formulierstatus, terwijl anderen de velden zouden opsplitsen in afzonderlijke useState oproepen.
Toen het tijd was om deze formulieren uit te breiden, stonden er vaak metagegevens zoals isFieldTouched zou worden toegevoegd, waardoor een rommelige component zou ontstaan met veel expliciet gedefinieerde lokale statussen om formulierinteracties af te handelen die gestandaardiseerd zouden moeten worden.
Deel 2: Uitvoering
Nu we de huidige valkuilen van onze codebase hebben geschetst, ga ik dieper in op de oplossingen en de besluitvorming achter elk daarvan.
Migreren naar Tailwind v4.0
Bij het evalueren van stylingoplossingen hadden we twee finalisten: Tailwind v4.0 en PandaCSS, omdat beide geen runtime-overhead hebben en het mogelijk maken om de markup en styling van een component op dezelfde locatie te plaatsen. De voordelen van PandaCSS waren typeveiligheid en een lage leercurve, aangezien ons bestaande ontwerpsysteem er erg vergelijkbaar uitzag.
De voordelen van Tailwind waren een volwassen ecosysteem en bekendheid bij ontwikkelaars. Veel van onze ingenieurs hadden Tailwind gebruikt voor persoonlijke projecten en waren er erg enthousiast over, waaronder ikzelf. Er zijn fantastische tools met ingebouwde Tailwind-ondersteuning, zoals Supernova en Tailwind-variantenAI-coderingshulpmiddelen zijn ook uitstekend in het schrijven van Tailwind-klassennamen, waardoor migratie veel minder lastig is.
Om de prestatievoordelen van het ontbreken van runtime-overhead te visualiseren, volgen hier de flame charts van dezelfde bewerking (snel scrollen op een grote, gevirtualiseerde tabelweergave) in Emotion en Tailwind, voor en na de verandering:


Figma naar Code-pijplijn
Bij de migratie naar Tailwind wilden we een manier vinden om onze Figma-tokens en codebase synchroon te houden, waarbij Figma onze bron van waarheid is. Ons ontwerpsysteem in Figma wordt onderhouden door ons ontwerpbureau, Metakarbon, die de ontwerpbeslissingen neemt en de stylesheet met semantische tokens maakt.
Ons doel was om een pijplijn te hebben waarin alle wijzigingen aan het onderliggende ontwerpsysteem handmatig konden worden gecontroleerd en met minimale inspanning konden worden geïmplementeerd. We kozen voor Supernova om ontwerptokens uit onze Figma te halen en in code om te zetten, omdat ze een uitstekende Tailwind v4.0-naar-code-extractor hebben.
Tijdens het werken met onze css-output stuitten we op een interessant probleem: we wilden dat semantische tokens naar verschillende waarden zouden verwijzen, afhankelijk van de eigenschap waarop ze van toepassing waren. Hier is een illustratief voorbeeld:
Wij willen neutraal-subtiel-standaard om op te lossen in 3 verschillende tinten van lichtproductneutraal: 100, 500 en 1000 - als we echter alleen dit zouden gebruiken, zou Tailwind v4.0 --kleur thema-variabele zou voortbrengen bg-bg-neutraal-subtiel-standaard, rand-achtergrond-neutraal-subtiel-standaard, bg-tekst-neutraal-subtiel-standaard en vele andere classNames die zowel onlogisch als overbodig zijn in de naamgeving.
Daarom hebben we een aangepast buildscript geschreven om onze output te transformeren zodat we gebruik kunnen maken van de meer specifieke thema variabele naamruimte:
Wat resulteert in classNames zoals bg-neutraal-subtiel-standaard, grensneutraal-subtiel-standaard en tekstneutraal-subtiel-standaard - die allemaal hun eigen specifieke tinten hebben.
Dus als ons onderliggende ontwerpsysteem verandert, hoeven we alleen maar onze Supernova-exporter te draaien en de output in onze codebase te plaatsen om te beoordelen. Vervolgens zet ons buildscript de ruwe output automatisch om in een gebouwd CSS-bestand dat al onze pakketten gebruiken.
At Slash, we follow a philosophy of maintaining control over our tooling . This allows us to extend functionality quickly and prevent vendor lock in. It also helps us diagnose and fix issues directly. By having our build script, we aren’t locked into Supernova as a vendor, and can extend it - like adding support for divide to be the same color as border, or light vs dark mode themes.
Other examples include our query builder and JSON schema to typescript code generation pipeline.
Headless UI-bibliotheken: Base UI en React Aria
Allereerst wil ik uitleggen waarom we een headless UI-bibliotheek wilden, en geen gestileerde oplossing. Het hele doel van de facelift is om onze merkidentiteit duidelijk tot uiting te laten komen in ons product. Het gebruik van een vooraf gestileerde oplossing zoals Shadcn of MaterialUI zou dit doel tenietdoen door onze componenten vooraf te stylen.
Vóór de facelift gebruikten we Radix UI, maar we waren niet zeker over zijn toekomst, toen we zagen dat de belangrijkste beheerders overstapten naar Base UI. Base UI is uitgegroeid tot een eenvoudige en uitbreidbare bibliotheek die zeer goed aansluit bij onze behoeften en uitstekend voldoet aan de toegankelijkheidseisen. Dankzij de uitstekende beheerders hebben we er alle vertrouwen in dat Base UI in de toekomst zal meegroeien met de behoeften van Slash.
Voor bepaalde componenten die nog geen ondersteuning bieden voor Base UI (bijvoorbeeld onze datumkiezer), hebben we gekozen voor React Aria, onze tweede keuze in onze zoektocht naar een headless UI. React Aria is beproefd en voldoet strikt aan alle toegankelijkheidsnormen. Uiteindelijk hebben we onze componenten bovenop Base UI gebouwd, omdat de aanpak van React Aria nogal eigenzinnig is en vereist dat we volledig instemmen met het React Aria-ecosysteem, en we gaven de voorkeur aan iets met minder mentale overhead.
Samengestelde componenten
Zoals eerder vermeld, zijn props-gebaseerde componenten in eerste instantie elegant, maar hebben ze te kampen met een slechte uitbreidbaarheid. Onze oplossing voor het creëren van meer uitbreidbare componenten was om sterk te leunen op de samengestelde componentarchitectuur, die de markup-controle van een component overdraagt aan de bovenliggende component. Het verschil tussen beide kan het beste worden geïllustreerd aan de hand van een voorbeeld. Hier volgt een vereenvoudigde versie van onze oude en nieuwe doorzoekbare selectiecomponent:
En hier is onze nieuwe doorzoekbare selectiecomponent, die gebruikmaakt van een samengestelde componentarchitectuur:
Op het eerste gezicht zijn de samengestelde componenten omslachtiger: de ontwikkelaar moet het markup-patroon kennen en volgen. Het voordeel is dat het markup flexibel kan worden uitgebreid, aangezien de volledige controle bij de ouder ligt. Daardoor komen markup-hacks nooit in de kerncomponent terecht. Als een engineer een voettekst aan de selectie moet toevoegen? Voeg deze dan gewoon toe aan het markup – dat is alles. renderSelectFooter prop naar de kerncomponent! Als een voettekst een veelvoorkomende vereiste wordt, maak dan een <SearchableSelect.Footer /> component!
Een andere veelvoorkomende valkuil van samengestelde componenten is dat ingenieurs implementaties van veelvoorkomende patronen die niet in de kerncomponent zitten, kunnen dupliceren. Daarom moet ons team zich meer bewust zijn van veelvoorkomende patronen en deze indien nodig aan de kerncomponent toevoegen. Je kunt samengestelde componenten ook gebruiken als de onderliggende API voor een component van één regel, maar we doen dit heel bewust om te voorkomen dat DEZE componenten godcomponenten worden. In een later hoofdstuk ga ik dieper in op hoe we kennisdeling aanpakken via Storybook.
Tailwind-varianten
Bij het daadwerkelijk stylen van onze componenten hebben we sterk geleund op Tailwind-variantenSommigen van ons hebben Autoriteit voor klassevariatie (CVA) en wilden dat soort variantlogica in onze stylingoplossing. Tailwind Variants had een een paar functies die ons ertoe hebben aangezet om het te gebruiken, waarbij de slots-API en de ingebouwde conflictoplossing de belangrijkste zijn.
Uiteindelijk hadden we gewoon een oplossing nodig die onze stylingvariantcode overzichtelijk hield en voldoende uitbreidbaar was om onze samengestelde componentbenadering te ondersteunen. Door Tailwind Variants te gebruiken, hebben we onze oude conditionele stylingcode aanzienlijk gestandaardiseerd, waardoor verschillende tokens expliciet werden toegepast op de emotiegestileerde div op basis van de props.
Hieronder vindt u een illustratief voorbeeld van de implementatie en het gebruik van onze knop:
Zoals u kunt zien, is de beller van Knop hoeft eigenlijk niet te gebruiken knopvarianten - het geeft props door aan Knop als configuratieopties, die vervolgens worden gebruikt om knopvarianten intern. We maken ook gebruik van de verlengen functie om stijlen te delen tussen vergelijkbare componenten, zoals invoervelden en selectievelden.
Je zult ook merken hoe gemakkelijk het is om de stijlen te scannen dankzij onze semantische klassennamen.
Testen en documentatie: Storybook + Chromatic
In de eerste helft van het project werkte ik alleen en hechtte ik niet genoeg waarde aan testen en documentatie. Toen ik echter de basiscomponentbibliotheek had voltooid en meer ingenieurs zich aansloten om hun respectievelijke producten te vernieuwen, werd het steeds duidelijker dat we een centrale plek nodig hadden om zeer eenvoudige vragen te beantwoorden, zoals "hebben we deze component?" en "hoe gebruik ik deze component op de juiste manier?".
Een van onze nieuwe ingenieurs, Sam, heeft in zijn eentje onze Storybook- en Chromatic-testsuite opgezet. Zonder deze tools zouden we heel snel in dezelfde valkuilen van technische schulden door verkeerd gebruik van componenten zijn terechtgekomen, vooral gezien de leercurve van samengestelde componenten.
Met Storybook kunnen we gebruiksvoorbeelden voor alle componenten centraliseren, waardoor die vragen volledig kunnen worden beantwoord met codevoorbeelden. Het wordt ook meteen duidelijk welke component een engineer moet gebruiken en of deze de configuratieopties ondersteunt die hij nodig heeft.
Chromatic detecteert visuele regressies, zodat elke wijziging aan de onderliggende componenten onmiddellijk wordt gemarkeerd als deze visuele veranderingen tot gevolg heeft. Hierdoor wordt het samenvoegen geblokkeerd totdat deze wijzigingen handmatig zijn gecontroleerd.
Formulierbeheer: Zod versus ArkType
De codebase van Slash is volledig geschreven in Typescript, en we maken daar optimaal gebruik van door tijdens het bouwen gedeelde typen te genereren in onze codebase. We wilden een oplossing voor formulierbeheer die het voor ontwikkelaars moeilijk maakte om zichzelf in de voet te schieten, en die gebruikmaakte van onze uitgebreide typeveiligheid.
We hebben uiteindelijk voor ArkType gekozen, omdat het zo sterk gekoppeld was aan Typescript dat we elke vorm van afwijking konden voorkomen. We konden onze gegenereerde types één op één koppelen aan ArkType-types met behulp van voldoet, zodat als ons onderliggende type verandert, onze codebase typefouten zal genereren.
Hier is een illustratief voorbeeld:
Hoewel Zod en React Hook Form ons goed van dienst hadden kunnen zijn, wilden we iets dat direct gebruikmaakte van TypeScript, en ArkType was hier duidelijk de beste keuze. We hebben Tanstack Form verkozen boven React Hook Form vanwege de striktere naleving van TypeScript.
Wat je meteen opvalt aan Tanstack-formulieren, is dat ze je dwingen om een strikte, eigenzinnige formulierstructuur te gebruiken. Hoewel dit een leercurve met zich meebrengt, vonden we het de moeite waard om standaardisatie af te dwingen in de manier waarop we formulieren schrijven in onze codebase.
Implementatie: Feature flag en code branching
Toen we daadwerkelijk code gingen schrijven voor de facelift, wilden we één enorme PR vermijden, omdat we naast de facelift nog steeds nieuwe functies in het oude ontwerpsysteem leverden.
We hebben een functievlag, en heb simpelweg de functie-vlag ingeschakeld voor alleen ons account, waardoor we hondenvoer onze eigen wijzigingen. We hebben ook een eenvoudige werkbalk geïmplementeerd om de facelift in en uit te schakelen, waardoor het heel eenvoudig is om regressies op te sporen door de facelift snel in en uit te schakelen.

Wat betreft de daadwerkelijke codevertakking, wilden we de bedrijfslogica-code niet dupliceren, en deden we dat als volgt:
Het bovenstaande voorbeeld illustreert een ideaal geval. - de realiteit van het introduceren van enorme hoeveelheden nieuwe code in een bestaande codebase is veel rommeliger. Bijvoorbeeld: als je de isFacelift Als je een hoog niveau (bijvoorbeeld op het niveau van de bovenste pagina) wijzigt, moet je noodzakelijkerwijs alle bedrijfslogica voor alle takken onder die component dupliceren. Maar als je de controle op het niveau van de afzonderlijke component uitvoert, ben je gebonden aan de oude markup-logica van de bovenliggende component, die vaak toch een opknapbeurt nodig heeft.
Wat ons beschermde tegen productieproblemen was dat we al deze functionaliteit achter een feature flag verborgen, zodat voor alle daadwerkelijke klanten, isFacelift was altijd onwaar totdat we klaar waren. Intern zouden we het ingeschakeld houden, zodat we regressies konden opsporen zodra ze in het platform terechtkwamen.
Toen de facelift volledig was, zijn we een samenwerking aangegaan met klanten waarmee we een nauwe band hadden en hebben we hen de mogelijkheid geboden om deze uit te schakelen, om feedback te vragen en regressies op te sporen zonder bestaande workflows te blokkeren.
Opmerkingen over de uitvoering
Hoewel het bovenstaande gedeelte op een zeer overzichtelijke manier is weergegeven, was dit geenszins de volgorde van implementatie. In werkelijkheid werden deze beslissingen genomen door dingen uit te proberen en snel te herhalen. Bijvoorbeeld:
- Mijn eerste versie van ons CSS-buildscript spammde de
@utiltyrichtlijn om elke gewenste aangepaste klassenaam te creëren - vervolgens bouwde Sam daarop voort door dit te vinden github-discussie waarin het thema 'tailwind' en variabele naamruimten werden beschreven. - Er waren twee iteraties van onze doorzoekbare enkelvoudige selectiecomponent met verschillende BaseUI-componenten voordat we onze huidige aanpak hebben gekozen, waarbij we gebruikmaken van de menu API.
Dit alles om te zeggen dat we deze beslissingen niet zo elegant hebben genomen als de hierboven beschreven afwegingen. De rode draad is voortdurende iteratie zonder de verlamming van het gevoel dat je het de eerste keer perfect moet doen.
Deel 3: Lessen
Ik heb persoonlijk veel geleerd van een project waarbij de scope het hele frontend omvatte. Ons team heeft ook veel geleerd, vooral omdat meer engineers hebben bijgedragen aan het opknappen van hun respectievelijke producten.
"De lat hoger leggen"
Een belangrijke reden waarom er überhaupt technische schulden zijn opgebouwd, was een gebrek aan gedeelde kennis. Bij een jonge start-up is dit geen probleem, omdat iedereen nauw samenwerkt. Maar naarmate een team groeit en een product zich verder uitbreidt, wordt het onmogelijk om aannames en gebruikspatronen net zo snel te delen als voorheen.
Onze filosofie voor de facelift was om 'de lat hoger te leggen'. We willen het moeilijk maken om slechte frontend-code te schrijven, wat betekent dat we moeten documenteren hoe goede frontend-code eruitziet en fouten moeten opsporen voordat ze in productie komen. Storybook en Chromatic zijn hiervoor van cruciaal belang en zijn de investering om ze op te zetten meer dan waard. Elke inspanning die je levert om de lat hoger te leggen, zal een samengesteld rendement opleveren op de kwaliteit van de code in de hele codebase – en omgekeerd.
De realiteit van langdurige projecten
De facelift duurde van begin tot eind ongeveer 4 maanden van continu werk, met aan het einde een speciale "hackweek" van 3 weken waarbij 6 andere engineers aan de facelift hebben meegewerkt (veel liefs voor hen <3). Ik had ook een uitstekende PM (Andy) die hielp bij het in kaart brengen van het hele project, de communicatie met Metacarbon en het geven van feedback over nieuwe UX-patronen.
Omdat de facelift geen project was dat ik in delen kon uitbrengen, heb ik heel lang niet het plezier gehad om producten aan klanten te leveren. Ik heb geleerd dat het een goede aanpak is om intern van context te wisselen: soms werkte ik aan de kerncomponentbibliotheek, vervolgens paste ik die kerncomponenten toe op enkele volledige pagina's, en wisselde ik tussen die twee. Deze aanpak helpt je ook om snel randgevallen in het gebruik van je kerncomponenten op te sporen, waardoor je gedwongen wordt om terug te gaan en betere abstractielijnen te trekken.
Het kan demotiverend zijn om tonnen code te herstructureren zonder dat de klant daar iets voor teruggeeft. Een paar dingen hielden me gemotiveerd:
- Naar mijn werk gaan was nog steeds elke dag leuk dankzij de mensen bij Slash!
- Ik kon nog steeds intern pronken met de voortgang. Het geeft een soort dopamine-kick om iemand te laten zien hoeveel vooruitgang je boekt, en dat is een geweldige manier om gemotiveerd te blijven, vooral als je van je vak houdt.
- Dit project zou de basis leggen voor onze frontend-codebase voor hopelijk een lange tijd, dus het was de moeite waard om het goed te doen.
- Slash groeit en wint, wat het TOTAAL motivatie.
Deel 4: Wat nu?
Nu onze facelift-uitrol in volle gang is, wordt het opsporen van randgevallen en regressies onze belangrijkste prioriteit. Gelukkig hebben we een supportteam van wereldklasse dat onze klanten door deze overgang heen helpt en problemen razendsnel aan het licht brengt.
Als u ons werk wilt bekijken, kunt u dat doen op onze demowebsite, waar je ook de functies van Slash kunt bekijken!
Als je hier bent beland en een getalenteerde ingenieur bent die problemen voor echte klanten wil oplossen bij een snelgroeiende start-up, solliciteer dan. hier. We horen graag van u.