Artykuł „Jak wykorzystać css-in-js w aplikacji opartej na React.js”

Rozwiązania pokroju CSS w JS w ostatnich latach zdobyły sporą popularność. Niemniej wydaje się, że nie idą za tym sensowne argumenty. Czy artykuł Przemka Suchodolskiego Jak wykorzystać css-in-js w aplikacji opartej na React.js w końcu mnie przekona do nowego podejścia?

Nie będę Was długo trzymał w niepewności: nie, nie przekonał. Mam wrażenie, że w zakresie CSS w JS od lat powtarzane są dokładnie te same argumenty, które z roku na rok wręcz są upraszczane w swojej formie. Z sensownie brzmiących argumentów zeszliśmy do poziomu powtarzanych frazesów, które mało co znaczą. I to, niestety, widać także w tym artykule.

  • Niemniej zacznijmy od czegoś zupełnie innego, co po prostu drażni mnie niemożliwie. Forma styli jest niepoprawna, jedyną akceptowaną formą jest stylów.
  • Rozwiązawszy ten językowy problem, przejdźmy do sensowniejszych argumentów:

    W 2012 roku światło dzienne ujrzał React, a wraz z nim składnia JSX, która łączy de facto HTML i JS (znaczniki HTML umieszczamy w kodzie JavaScript).

    Tak po prawdzie, JSX łączy XML z JS-em – podobnie jak dawny E4X. Gdyby JSX faktycznie pracował zgodnie z zasadami HTML-a, nie działałyby w nim samozamykające się tagi i kod

    <Component>
    	<ComponentElement />
    	<span>Test</span>
    </Component>

    zostałby wyrenderowany jako

    <Component>
    	<ComponentElement>
    		<span>Test</span>
    	</ComponentElement>
    </Component>

    Łatwo to udowodnić, wykorzystując wbudowany w przeglądarkę parser DOM.

  • Pewnie nie raz zdarzyło się Wam soczyście przeklnąć przed monitorem, kiedy okazywało się, że selektor, który chcecie ostylować, jest gdzieś wyżej tak „złożony”, że jedynym wyjściem z całej sytuacji jest dodanie !important. A co jeśli ktoś wyżej już dał !important?

    Jak dla mnie to bardzo nietrafiony argument przeciwko CSS-owi. Pokazuje on bowiem, jak wielu developerów nie rozumie CSS-a, a nie jakie problemy wynikają z jego wykorzystania. Nieumiejętność korzystania z kaskadowości czy specyficzności jest winą ewidentnie leżącą po stronie programisty i nie naprawi tego żadna nowa technologia. W takim wypadku będzie to po prostu przesuwanie problemu w inne miejsce.

  • Osobiście uważam, że ten kto wymyślił BEM’a zasługuje na darmowy wjazd na wszystkie konfy dotyczące CSS’a. Z drugiej strony, tak jak powiedziałem, to są tylko praktyczne wskazówki, konwencja w pisaniu kodu. Możesz z nich korzystać, ale nikt Cię do tego nie zmusi. Co więcej możesz też źle interpretować niektóre z tych wskazówek, co w konsekwencji może doprowadzić do jeszcze gorszej struktury Twojego kodu CSS.

    Nie, BEM nie jest tylko konwencją – BEM jest całą architekturą. Tak po prawdzie istnieje wręcz oficjalny ekosystem narzędzi do pracy z nim. Z kolei na samym poziomie teoretycznym BEM ustala nawet strukturę plików projektu.

    Nie mogę się także zgodzić, że to tylko praktyczne wskazówki, ponieważ oficjalny standard HTML też jest tylko wskazówką. Niemniej skoro już decydujemy się na używanie odpowiedniej metodyki, nie widzę sensu w jej świadomym łamaniu.

    Nie trafia do mnie także argument o możliwości złej interpretacji. Owszem, jest to możliwe, ale jest to też możliwe przy wykorzystywaniu CSS-a w JS. Nie ma technologii czy konwencji, której nie dałoby się źle zinterpretować. Poza tym: to kolejny argument, który przerzuca ciężar na postać programisty. Fakt, że ktoś źle zinterpretuje BEM, nie jest problemem z BEM-em per se.

  • Developerzy z Facebooka pokazali światu „nowe” podejście do tworzenia aplikacji webowych — React.js. Podstawą tego podejścia były komponenty. Klocki, z których budujemy poszczególne fragmenty UI’a. Przestajemy mówić o skomplikowanych widokach wykorzystujących mniejsze szablony wypełniane danymi. Teraz wszystko jest komponentem.

    Problem polega na tym, że podejście Reacta wcale nie było takie nowe. AngularJS, który pojawił się wcześniej (wg Wikipedii ok. 2,5 roku wcześniej), przecież też miał podejście komponentowe (wraz z możliwością definiowania własnych tagów HTML!). Ba, pomysł Web Components pojawił się w 2011 roku. A przecież to są też czasy prac nad ES Harmony w świecie już modularnego JS-a (dzięki CommonJS czy AMD). To też czasy, w których od dawna istnieje OOCSS. W końcu to czasy BEM-u, którego bloki nie są niczym innym jak niezależnymi, samodzielnymi komponentami.

  • I to właśnie do tej ostatniej było wiele uwag i pretensji: „ale jak to!? HTML w JS!? — Oszaleli! Co z ‚separation of concerns’!?…blablabla”. Z czasem okazało się, że do wszystkiego idzie się przyzwyczaić i nie taki diabeł straszny jak go malują.

    Ten fragment brzmi, jakby podział obowiązków (ang. separation of concerns) był mało istotny. A przecież jest wręcz odwrotnie, co PE udowadnia od bardzo dawna. Zresztą problem z uzależnianiem HTML-a od JS-a (a zatem problem opóźnienia renderowania czegokolwiek aż JS nie zostanie pobrany i sparsowany) był także niezwykle widoczny w ekosystemie Reacta. Wypada wspomnieć, że SSR – coś, co po prostu działało przez ponad 20 lat – musiało zostać odkryte na nowo.

    Poza tym na ten problem można spojrzeć z nieco innej strony i zamiast o podziale obowiązków, mówić o podziale technologii (ang. separation of technologies). Choć te pojęcia mówią o zupełnie różnych zjawiskach, bardzo często – właśnie w kontekście Reacta – są mylone. Bierze się to z faktu, że od zawsze podział obowiązków był nierozerwalnie związany z podziałem technologii. HTML stanowił warstwę treści, CSS warstwę prezentacji, a JS – logiki. Wprowadzenie do tego równania JSX-a sprawia, że warstwa treści zostaje przeniesiona do kodu JS. Z jednej strony ułatwia to pracę, z drugiej – wprowadza zagrożenie, że usunięcie podziału technologii usunie też podział obowiązków. W końcu kod logiki i treści znajduje się w tym samym miejscu. Jeśli do tego dodamy CSS w JS, otrzymujemy dość umowny podział obowiązków w obrębie często jednego pliku. Nie bez przyczyny de facto standard PSR-1 w PHP wymaga rozdziału logiki od prezentacji na różne pliki. Nie dość, że eliminuje to kruchość podziału, to dodatkowo podnosi czytelność. Stąd uważam, że podejście Vue z jego jednoplikowymi komponentami jest lepsze od podejścia Reacta, w którym wszystko jest w JS.

    Niemniej nawet jeśli zastąpimy pojęcie podziału obowiązków pojęciem podziału technologii, nie rozwiąże to podstawowego problemu: bez sensownego SSR, wydajność renderowania w React jest zauważalnie niższa niż statycznej strony. Stąd zresztą pojawiła się potrzeba przejścia od frameworków do kompilatorów. Doskonałym przykładem takiego podejścia może być Svelte.

  • Ale czy to rozwiązuje nasz problem? No nie bardzo. Musimy w dwóch miejscach utrzymać nazwę klasy css. Ponadto nikt nie zabroni innemu developerowi zadeklarować klasy o tej samej nazwie w innym pliku css.

    Po raz kolejny odnoszę wrażenie, że CSS w JS ma rozwiązywać problem nie z technologią, a z używającymi jej programistami. A przecież tego typu problemy zostały rozwiązane wraz z wprowadzeniem metodyk pokroju BEM. Nazwy bloków i plików są ściśle określone i wiadomo, jak będą się nazywać poszczególne klasy. Co więcej, przy dobrze zaprojektowanym, atomowym komponencie nie będzie też dochodzić do zmian w nazewnictwie. Z góry jest wykluczona także możliwość nadania tej samej nazwy klasy w innej części systemu. Jedyne, co wystarczy zrobić, to stosować się do przyjętej metodyki. A to nie jest problem z CSS-em, a właśnie z programistami.

    Zresztą jeśli już działamy na systemie tak ustrukturyzowanym, jak BEM, to przecież automatyzacja tego typu rzeczy jest banalnie prosta.

  • W sumie to na tym moglibyśmy zakończyć o stylowaniu React’a. Uzyskaliśmy modułowy i re-używalny kod CSS. Nigdy więcej konfliktów! Mamy bezpośrednie określenie zależności i unikamy globalnej przestrzeni nazw CSS

    I znów: problem modułowości i ponownego użycia CSS-a metodyki pokroju BEM rozwiązały bardzo dawno. Przy założeniu, że wykorzystujemy BEM-ową strukturę plików, wystarczy skopiować katalog z komponentem do nowego projektu. Zależności są równie wyraźnie zaznaczone.

    Co do problemu globalnej przestrzeni nazw: tak po prawdzie moduły CSS (CSSM) rozwiązują ten problem dokładnie tak samo jak BEM. W obydwu rozwiązaniach nie likwidujemy globalnej przestrzeni nazw, a jedynie ją oszukujemy poprzez używanie unikalnych nazw klas. W przypadku BEM to my je ustalamy, w przypadku CSSM – program sam je generuje. I to jest po prawdzie główna różnica między tymi podejściami. By faktycznie wyeliminować problem globalnej przestrzeni nazw, należy użyć Shadow DOM.

  • Cała magia css-in-js polega na wykorzystaniu funkcjonalności języka JS ‚Template literals’, którą możemy cieszyć się od wersji ES6.

    Z czysto technicznego punktu widzenia nie jest to do końca prawdą. Wykorzystanie biblioteki styled-components jest możliwe też w ES5:

    var styled = require( 'styled-components' ).default,
    	styles = styled.button( [ 'padding: 16px;' ] );
    
    console.log( styles );

    Co więcej, w takiej formie pewnie trafi to ostatecznie do przeglądarki.

  • const MyAwesomeComponent = () => (
    	<div className="awesome">
    		<div className="awesome__header awesome__header--highlighted">
    		</div>
    		<div className="awesome__content">
    			<div classsName="content__subelement"></div>
    				...
    		</div>
    	</div>
    );

    Ten przykład jest skrajnie tendencyjny, bo założenie jest proste: nie można wydzielić podkomponentów. Niemniej przykład wykorzystujący styled-components w całości opiera się na wydzielaniu podkomponentów! W końcu tym są AwesomeHeader, AwesomeContent itd. Jeśli przyjmiemy te same założenia dla obydwu kodów, wówczas różnica w czytelności staje się minimalna:

    const AwesomeHeader = () => (
    	<div class="awesome__header awesome__header--highlighted">
    	</div>
    );
    
    const AwesomeContent = ( props ) => (
    	<div class="awesome__content">
    		{ props.children }
    	</div>
    );
    
    const MyAwesomeComponent = () => (
    	<div className="awesome">
    		<AwesomeHeader />
    		<AwesomeContent>
    			...
    		</AwesomeContent>
    	</div>
    );

    Poza tym przy rozumieniu BEM-u jako DSL-a, klasy są o wiele bardziej informacyjne niż lista stylów. Nie określają bowiem prezentacji elementu, a jego pozycję w strukturze strony, jego rolę.

  • Nadchodzi czas refaktoryzacji. Kod CSS i JS trzymany jest osobno. […] Zmieniacie albo usuwacie daną klasę css i nasuwa się pytanie: co z resztą miejsc, w których wykorzystywana jest ta klasa css? Czy mogę ją bezpiecznie usunąć? […] Przy podejściu css-in-js unikamy tego problemu. Jasno określamy zależność między komponentem a jego stylowaniem.

    I znów: to problem, który BEM rozwiązał dawno temu. Przy odpowiedniej strukturze plików, zmiany w obrębie komponentu zachodzą… wyłącznie w obrębie komponentu. Dopóki jego zewnętrzne API się nie zmienia, zmiany w żaden sposób nie wpływają na całą resztę aplikacji. Wystarczą dwa proste założenia:

    1. Każdy komponent jest w pełni niezależny, więc nie istnieją style globalne (co najwyżej globalne zmienne ustawiające podstawowe parametry wyglądu, jak np. główny kolor).
    2. Każdy komponent ma swój wydzielony katalog, w którym posiada swoje własne szablony, style i logikę.

    W tym zakresie CSS w JS po prostu przenosi problem w inne miejsce i oferuje inne rozwiązanie.

  • Owszem możemy przeszukać nasz kod pod kątem wystąpienia nazwy klas, ale jest to bardziej czasochłonne podejście i nie zawsze gwarantuje sukces.

    Owszem, pod warunkiem, że robimy to ręcznie. Są jednak odpowiednie narzędzia do tego typu refaktoryzacji, jak np. uncss.

  • Użycie CSS-in-JS sprawia, że wystąpienie FOUC jest niemalże niemożliwe. Nasze style dołączone są do komponentów, więc jeśli nasz komponent został przeparsowany przez silnik JS to mamy pewność, że nasze style też są już dostępne.

    Problem w tym, że to jest równocześnie największa wada CSS w JS: opóźnianie renderowania strony.

    Dodatkowo problem FOUC został rozwiązany 10 lat temu. Sam sposób wczytywania CSS-a niejako wyklucza możliwość wystąpienia FOUC, ponieważ arkusze stylów blokują renderowanie do czasu ich wczytania. To domyślne zachowanie otwiera drogę dla wielu ciekawych optymalizacji, jak np. renderowanie strony we fragmentach. Dodatkowo istnieją ogólnie znane dobre praktyki pozwalające na wydajne wczytywanie CSS-a.

    Prawda jest taka, że dopóki czegoś nie zepsujemy, FOUC nie powinien wystąpić. Jeśli występuje, to powinniśmy przemyśleć sposób, w jaki wczytujemy style.

  • Wait wait wait! WAT!? Słyszeliście kiedyś o testach jednostkowych Waszego kodu CSS? No właśnie! Do tej pory nie było to możliwe — no bo jak?

    Oczywiście, że było możliwe – zarówno na poziomie API (porównywanie stylów), jak i na poziomie wizualnym. Powstał cały szereg narzędzi do automatycznego testowania CSS-a.

  • Pomijając fakt, że biblioteki typu jsdom mogą różnie implementować funkcję do wyciągania styli.

    Bardzo mało z wyżej wymienionych narzędzi korzysta z emulatora przeglądarki. Nie ma potrzeby stosowania takich sztuczek w czasach, gdy mamy Selenium, WebDriver i Puppeteera.

  • Przy użyciu css-in-js możemy łatwo przetestować nasz kod css poprzez snapshot-testing. Polega to na tym, że nasz test runner np. jest zapisuje do pliku tekstowego wynik funkcji renderującej.

    Uważam to za spory krok w tył w porównaniu do wymienionych narzędzi automatyzujących wizualne testy porównawcze. Nie dość, że wracamy tak naprawdę do porównywania wyłącznie stylów nadanych elementowi, a nie jego faktycznego renderowania, to dodatkowo nie testujemy tego w rzeczywistych przeglądarkach. Jedyne, co testujemy, to fakt, czy funkcja renderująca dodaje elementowi odpowiednie style.

  • Koniec z edytowaniem index.html! Teraz wystarczy tylko install i import.

    Przy założeniu, że tworzymy całość ręcznie. Niemniej istnieją przecież odpowiednie narzędzia, które generują HTML z kodu komponentów, np. bem-xjst.

    Poza tym: czy faktycznie dołączenie arkusza stylów aż tak bardzo różni się od importowania biblioteki…?

  • Trzymając CSS’y w plikach JS i dodatkowo korzystając z techniki code-splitting ładujemy tylko niezbędne w danej chwili style. Zupełnie odwrotnie rzecz się ma przy tradycyjnym podejściu, czyli bezpośrednim importowaniu css’ów i wydzielaniu ich do osobnego pliku .css.

    Swego czasu na swojej stronie domowej miałem system wypluwający zoptymalizowane arkusze stylów w zależności od podstrony. W połączeniu ze wspomnianymi już narzędziami pokroju uncss taki system jest w stanie generować arkusze zawierające wyłącznie niezbędne dla danej podstrony style.

    No i chyba nie muszę po raz kolejny wspominać, jak banalne jest to do osiągnięcia w przypadku BEM i jego ścisłego podziału na bloki, który jest wyrażany również w strukturze katalogów…

  • Jak to CSS’y w JS’ie!? To nie „po bożemu”!? Z takim nastawieniem musicie się liczyć jeśli w zespole macie osoby „sceptycznie” nastawione do nowości typu css-in-js. Można to też uznać za minus.

    Po raz kolejny marginalizowany jest problem podziału obowiązków. A przecież jest to jedna z podstawowych zasad, które leżą de facto u podstaw całej otwartej Sieci.

    Poza tym jeśli osoba taka jak Lea Verou zdecydowanie negatywnie wypowiada się o CSS w JS (i to naprawdę negatywnie), to wypada się głębiej zastanowić nad tym, czy aby na pewno mamy rację. I czy nasze argumenty faktycznie są takie mocne.

Po raz kolejny powtórzę: nie neguję, że CSS w JS ma swoją niszę i że często faktycznie łatwiej jest wdrożyć tego typu rozwiązanie niźli np. pełnoprawny ekosystem komponentów oparty na BEM. Nie można jednak sugerować, że CSS w JS rozwiązuje pewne odwieczne i nierozwiązywalne problemy, podczas gdy inni już dawno je rozwiązali albo gdy to wcale nie są problemy…

2 myśli na temat “Artykuł „Jak wykorzystać css-in-js w aplikacji opartej na React.js””

  1. Biorę udział w dwóch dużych projektach Reactowych. Jeden jest w BEMie, drugi na Styled Components. Nie mam nic do BEMa – jest super, ale praca w Styled Components jest dla mnie dużo przyjemniejsza, szybsza, intrygująca.

    Dyskusja czy mieszać HTML z JSem (JXS) albo CSS z JSem wydaje mi się nieco akademicka. Trochę jak o wyższości Angulara nad Reactem, TypeScripta nad Flow, WebStorma nad VSC czy Androida nad iOSem. Rozumiem, że są argumenty za i przeciw, ale koniec końców najważniejsze jest to w czym nam się najwygodniej pracuje.

    1. Muszę przyznać, że faktycznie – na poziomie teoretycznym jest to nieco akademicka dyskusja. Niemniej najczęściej bierze się ona z tego, że nowoczesne rozwiązania często wykorzystywane są w sposób, który nie bierze pod uwagę bagażu doświadczeń, jakie całe środowisko wyrobiło sobie przez ponad 20 lat. I właśnie na tym styku dyskusja przestaje być akademicka, stając się o wiele bardziej praktyczna. Doskonałym przykładem tego może być właśnie kwestia SSR, które dawniej było uważane za fundamentalną rzecz, a w świecie Reacta musiało zostać odkryte na nowo.

      Jak dla mnie największym problemem CSS w JS jest fakt, że próbuje rozwiązać problemy, które już zostały wcześniej rozwiązane lub wynikają z niezrozumienia CSS-a (czy też: niechęci do tego, że nie jest on bardziej JS-owy), powtarzając przy tym błędy wcześniejszych rozwiązań. CSS w JS jest po prostu niedojrzały moim zdaniem i jeszcze trochę mu to zajmie, by dojść do tego, do czego wcześniej doszedł BEM czy jeszcze wcześniej PE.

      Z drugiej strony nie można nie zauważyć, jak silny nacisk kładzie CSS w JS na DX. Być może to jest właśnie to, co przekonuje do tego rozwiązania coraz większe rzesze osób. Niemniej nie można przy tym zapominać, że zawsze ważniejsze powinno być UX. Coraz częściej jest to dostrzegane i np. porównuje się JS do dwutlenku węgla – niezbędnego, ale też i szkodliwego w nadmiarze. Stąd osobiście czekam na ewolucję CSS w JS z prostego narzędzia do przerabiania kodu CSS na JS na narzędzie, które będzie generować zoptymalizowany kod CSS i dostarczać go w sposób niezależny od JS-a, najlepszy dla użytkownika. Czyli znów: od frameworka w stronę kompilatora.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

This site uses Akismet to reduce spam. Learn how your comment data is processed.