Jak wykorzystać css-in-js w aplikacji opartej na React.js
Założę się, że gdybym spytał Was o trzy słowa kojarzące się z frontendem, odpowiedź byłaby prosta: HTML, CSS i JavaScript. W wielkim skrócie można powiedzieć, że na tych trzech technologiach oparte są aplikacje webowe. Zwykło się nie mieszać ich ze sobą gdyż każda z nich ma inne zadanie. Ale jak to w naszym frontendowym świecie bywa, wszystko się zmienia.
Przemek Suchodolski. Frontend developer z pasją i zamiłowaniem do JS’a. Fan „czystego kodu” i Test Driven Development’u. Z komercyjnym programowaniem związany od 2012 roku. W swojej karierze miał okazję pracować dla koreańskiej korporacji, amerykańskiego startup’u czy polskiego software-house’u. Nieśmiało stawia swoje pierwsze kroki jako prelegent oraz bloger (przemuh.pl).
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). Gdyby spojrzeć na drugą stronę sceny np. na Angulara czy Vue, to także możemy zobaczyć połączenie HTML i JS — tyle, że w drugą stronę — fragmenty JS’a wkładamy do HTML’a.
Ale nie o łączeniach HTML’a będzie dzisiaj mowa. Dziś porozmawiamy sobie o połączeniu, o którym jeszcze niedawno nie śniło się najstarszym góralom. Pokażę Wam jakie są zalety i wady używania CSS-in-JS. Zapraszam!
Spis treści
CSS
CSS (Cascading Style Sheet) jest językiem używanym do opisu formy prezentacji naszej aplikacji/strony WWW. Powstał w 1996 roku, a opracowała go organizacja W3C. Tyle definicji, a jak to wygląda w praktyce? Żeby nauczyć się CSS’a nie trzeba wiele czasu. Składnia i zasady są banalnie proste. Podstawą są selektory i rodzaje atrybutów. To z grubsza tyle!
Cała „trudność” kaskadowych arkuszy styli polega na utrzymaniu kodu. Globalna przestrzeń nazw wcale nam tego nie ułatwia. Sama kaskadowość również może przysporzyć więcej problemów, niż się tego spodziewamy. 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`?
Odpowiedzią na wyżej wymienione problemy z utrzymaniem kodu CSS są wszelakiej maści konwencje/metodologie takie jak: SMACSS, OOCSS czy bardzo popularny BEM. Dostarczają one wielu praktycznych wskazówek „jak pisać dobry, łatwiejszy w utrzymaniu kod CSS”. 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. Nasuwa się pytanie – „jak żyć panie developerze? jak żyć ?”. Zaraz do tego dojdziemy. Przed tym jednak chwila dla React’a.
Component based system
Na przełomie lat 2012/2013 zatrzęsła się ziemia a piekło zamarzło. 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. Z komponentu powstałeś i w komponent… dobra, nie tędy droga.
Jak do tego magicznego słowa komponent dodamy przymiotnik „re-używalny”, to robi się jeszcze ciekawiej. Oprócz podejścia komponentowego dostaliśmy też VirtualDOM i składnię JSX. 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ą. Dzisiaj nie znam osoby, która używałaby React’a bez JSX’a — uwierzcie mi to droga przez mękę, byłem tam i szybko uciekłem. JSX FTW!
No dobra, ale JSX to HTML i JS. A co z CSSem? Jak stylować te całe komponenty? Tego już nikt nie sprecyzował. React daje co prawda możliwość stylowania in-line poprzez przekazanie obiektu `style`, ale takie stylowanie nie jest najbardziej efektywnym sposobem pisania CSSów — łagodnie rzecz ujmując. To jak żyć?
Import style.css
Kiedy korzystamy z jakiejkolwiek formy „budowania/składania” naszej aplikacji przy pomocy bibliotek takich jak np. `webpack` czy `parcel` to pierwszą, bardzo naturalną rzeczą, jaka przychodzi nam do głowy to importowanie pliku ze stylami bezpośrednio w pliku z naszym komponentem:
```css // button.css .btn { border: 1px solid pink; } ``` ```javascript // Button.js import React from "react"; import "./button.css"; const Button = ({ children }) => ( <button className="btn"> { children } </button> ); export default Button; ```
Jeśli korzystamy z `webpacka` to przy użyciu specjalnego loadera nasz import pliku css zostanie poprawnie zinterpretowany i dołączony jako `<style>` tag. 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. Wtedy będzie liczyć się kolejność dołączenia takiego pliku — ostatni wygrywa! Nie o to jednak nam chodzi. Lećmy dalej.
CSS modules
Moduły CSS są bardzo podobne w składni do importowania pliku css, ale pod spodem dzieje się nieco więcej magii.
Nasz import przypisujemy do zmiennej, pod którą kryje się obiekt. Kluczami obiektu są nazwy zadeklarowanych klas css. Zachowując nasz plik css z poprzedniego przykładu zobaczmy jak wygląda teraz plik js:
```javascript // Button.js import React from "react"; import styles from "./button.css"; const Button = ({ children }) => ( <button className={ styles.btn }> { children } </button> ); export default Button; ```
To co trafi do naszego HTML’a pod atrybutem `class` wcale nie będzie ciągiem znaków `btn`. Pole `styles.btn` kryje string-hash utworzony na etapie budowania naszej aplikacji. Teraz mamy pewność, że nawet jeśli ktoś zadeklaruje taką samą klasę css w innym pliku, to nie nadpisze to nam naszych styli bo po zbudowaniu aplikacji nazwy tych samych klas będą zupełnie innymi ciągami znaków. 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. Ale spróbujmy pójść krok dalej.
CSS-in-JS
A co jeśli zamiast importować plik ze stylami, wrzucić by je tak bezpośrednio do kodu JavaScript zachowując przy tym `<style>` tag zamiast in-line styling? Nie ma problemu! Podejście takie nazywane jest css-in-js. Do jego użycia potrzebujemy dodatkowej biblioteki, która nam przetłumaczy nasz kod css-in-js do postaci wynikowej. Takich bibliotek jest cała masa, ja skupię się na bardzo popularnych `styled-components` i `emotion`, które mają bardzo podobne API.
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.
Zapewne wielu z Was używa tego do „sklejania” ciągów znaków i interpolacji zmiennych:
```javascript const are = "are"; const awesome = "AWESOME!"; const myText = `template literals ${are} ${awesome}`; ``` Oprócz interpolacji i konkatenacji ;) szablonów można użyć w kontekście funkcji: ```javascript function foo(strings, areExp, adjExp) { console.log("Strings", strings); console.log(areExp, adjExp); } foo`template literals ${are} -- ${awesome}`; // console.log: Strings ["template literals ", " -- "] // console.log: are, AWESOME! ```
Dokładnie z tej własności korzystają biblioteki css-in-js. Zobaczmy to na przykładzie `styled-components`:
```javascript import styled from 'styled-components'; const Title = styled.h1` font-size: 1.5em; text-align: center; color: red; `; export default Title; ```
Obiekt styled posiada metody, których nazwy odpowiadają elementom HTML. W naszym przykładzie skorzystaliśmy z h1, ale równie dobrze może to być div, span czy inny element HTML. Zaraz po nazwie znacznika skorzystaliśmy ze składni template literals. Pomiędzy znakami umieszczamy nasz kod CSS. I to wszystko! Po wyrenderowaniu naszego komponentu Title dostajemy ostylowany znacznik h1! Dodatkowo nie korzystamy tu z in-line styling. Nasz element HTML dostanie normalnie klasę css w postaci string-hash’a. Natomiast definicja tej klasy zostanie umieszczona w tagu <style> między znacznikami head na naszej stronie.
A co z „warunkowym” stylowaniem? Co jeśli chciałbym dodać styl tylko w momencie kiedy komponent otrzyma odpowiednie propsy? Nie ma problemu! W takim wypadku wystarczy, że skorzystamy z interpolacji:
``` import styled from 'styled-components'; const Title = styled.h1` font-size: 1.5em; text-align: center; color: ${props => props.main ? "red" : "blue"}; `; // <Title main> -> color: red // <Title> -> color: blue ```
>Jesteście fanami SASS’a i nie możecie żyć bez funkcji jakie daje compass? Nie ma problemu – skorzystajcie z biblioteki polished.js (to taki lodash dla cssów). Super! Ale po co mi to wszystko — zapytacie. Po co mieszać css’y z js’em?! Co ja z tego będę miał? Już tłumaczę.
Bardziej czytelny kod
Pierwszą z zalet jaka przychodzi mi do głowy to poprawa czytelności kodu. Wyobraźcie sobie, że budujecie bardzo skomplikowany komponent i z jakiś przyczyn nie możecie wydzielić mniejszych sub-komponentów. Do tego korzystacie z BEM’a…wynik końcowy może wyglądać tak:
```javascript const MyAwesomeComponent = () => ( <div className="awesome"> <div className="awesome__header awesome__header--highlighted"> </div> <div className="awesome__content" <div classsName="content__subelement"></div> ... </div> </div> ); ```
Więcej miejsca zajmują nazwy klas niż logika komponentu. Przy wykorzystaniu `styled-components` sprawa się upraszcza:
```javascript const AwesomeHeader = styled.div` font-size: 12px; ... `; const AwesomeContent = styled.div` ...styles `; // itd... const Awesome = () => ( <AwesomeWrapper> <AwesomeHeader> This is a header </AwesomeHeader> <AwesomeContent> <SomeOtherNestedComponent /> </AwesomeContent> </AwesomeWrapper> ); ```
W takim przypadku mamy jasny obraz tego z jakich komponentów składa się nasz główny komponent. Dodatkowo długie nazwy klas nie zaciemniają nam całości obrazu funkcji renderującej.
Eliminacja martwego kodu
>Nadchodzi czas refaktoryzacji. Kod CSS i JS trzymany jest osobno. Nie daj boże jedną nazwę klasy wykorzystujecie w wielu miejscach. Korzystacie z SASS’a — zagnieżdżając selektory… boom! Dochodzicie do wniosku, że chcecie zmienić wygląd komponentu. 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ąć? Czy taki ruch na pewno jest bezpieczny? Czy macie testy porównujące pixel po pixelu wygląd Waszej aplikacji? W 90% pewnie nie… Z mojego doświadczenia w większości przypadków kończy się to prośbą do działu QA o przetestowanie „czy czasem wygląd aplikacji po moich zmianach się nie rozjechał”.
Przy podejściu css-in-js unikamy tego problemu. Jasno określamy zależność między komponentem a jego stylowaniem. Jeśli korzystamy z biblioteki `emotion` to oprócz generowania ostylowanych komponentów mamy możliwość generowania re-używalnego stylu css w postaci klasy z hash-stringiem:
```javascript import { css } from "react-emotion"; const styles = css` font-size: 1.5em; color: red; `; export styles; ```
W powyższym przykładzie eksportujemy nic więcej niżeli nazwę klasy css. Nazwa ta oczywiście zostanie wygenerowana podczas działania naszej aplikacji. Ale wynikiem funkcji `css` jest ciąg znaków, który możemy wykorzystać w tradycyjny sposób przypisując go do atrybutu `className`:
```javascript import styles from "./styles.js"; //importujemy obiekt ze stylami const Component = () => <div classsName={ styles }>...</div> ```
Teraz jeśli dochodzimy do sytuacji, kiedy nasz `Component` nie jest już potrzebny od razu dostaniemy informację, że obiekt `styles` nie jest używany. Nowoczesne IDE takie jak np. Webstorm są w stanie przeanalizować cały kod aplikacji i powiedzieć nam czy style css-in-js są gdziekolwiek importowane. Jeśli nie — możemy je bezpiecznie usunąć. Tego samego nie zrobimy z czystym css’em. 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.
Podpunkt ten równie dobrze aplikuje się do podejścia css-modules.
FOUC — a co to takiego? a komu to potrzebne?
Spotkaliście się z terminem FOUC (flash of unstyled content)? O takim zjawisku mówimy, kiedy nasz kontent HTML załadował się przed stylami dołączonymi poprzez zewnętrzne pliki css. Trwa to zazwyczaj krótki okres czasu — po chwili nasze css’y są już załadowane. Nie zmienia to faktu, że UX w takim wypadku leży i kłuje w oczy. 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.
Unit testy kodu CSS
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? Jedyne co możemy sprawdzić to to, czy komponent dostał odpowiednią klasę. Ale posiadanie klasy css jeszcze o niczym nie świadczy. Ktoś mógł usunąć albo zmienić zawartość pliku css i nasz komponent może nie wyglądać tak jakbyśmy się tego spodziewali. Wyciąganie styli poprzez `window.getComputedStyle` nie wydaje się być dobrym pomysłem w kontekście testów jednostkowych. Pomijając fakt, że biblioteki typu `jsdom` mogą różnie implementować funkcję do wyciągania styli.
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. Może to wyglądać mniej więcej w ten sposób:
```javascript // src/components/Button.test.js import React from 'react' import styled from 'react-emotion' import renderer from 'react-test-renderer' const Button = styled.div` color: hotpink; ` test('Button renders correctly', () => { expect( renderer.create(<Button>This is hotpink.</Button>).toJSON() ).toMatchSnapshot() }); ``` ``` // Jest Snapshot exports[`Button renders correctly 1`] = ` .emotion-0 { color: hotpink; } <div className="emotion-0 emotion-1" > This is hotpink. </div> `; ```
Należy jednak pamiętać, że snapshot testy nie nadają się do TDD, za to mogą dodać nam pewności przy refaktoringu.
Dystrybucja komponentów
Kiedy dołączamy do naszego projektu jakąś bibliotekę js, która nie wystawia żadnych komponentów UI to schemat działania jest dość prosty:
1) npm install
2) import lib from „lib”
Gorzej jeśli chcemy zaimportować bibliotekę z komponentami UI. Najczęściej wiązało się to ręczną manipulacją w pliku `index.html`, w którym to należało dodać zewnętrzny arkusz stylów css.
CSS-in-JS przenosi nas na zupełnie inny poziom, jeśli chodzi o dystrybucję naszych komponentów. Cały kod piszemy w pliku JS, który na koniec exportujemy jako punkt wejściowy naszej biblioteki. Dołączamy do tego libkę css-in-js i tyle! Dla przykładu — wielkość biblioteki `emotion` po kompresji gzip to tylko ~5KB! Taki sposób dystrybucji jest o wiele łatwiejszy i nie wymaga dodatkowych kroków od użytkownika! Koniec z edytowaniem `index.html`! Teraz wystarczy tylko `install` i `import`.
Ładowanie tylko niezbędnych styli
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`. Wtedy na starcie naszej aplikacji ładujemy wszystkie deklaracje css, które de facto mogą być niepotrzebne — użytkownik może nigdy nie dotrzeć do miejsca aplikacji, które zostało specyficznie ostylowane.
A co z minusami?
Jeśli chodzi o ciemną stronę css-in-js to na pewno trzeba wspomnieć o wydajności. Trzymając kod CSS w plikach JS dodajemy narzut na przeparsowanie naszego kodu. O ile nie ma to większego wpływu na użytkowników korzystających z naszej aplikacji na desktopach tak użytkownicy mobilni, szczególnie ci z nieco gorszymi telefonami, mogą już odczuć narzut na czas parsowania.
Dla innych minusem może być wprowadzenie kolejnej zależności do projektu. Jakoś tą składnię css-in-js trzeba „przetłumaczyć”. Styled-components są chyba najbardziej popularną biblioteką, ale na rynku jest wiele alternatyw. A ich ilość także może przyprawić o ból głowy.
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.
Podsumowanie
CSS-in-JS zyskuje coraz bardziej na popularności. Sprawia, że style w naszej aplikacji są łatwiejsze w utrzymaniu, składanie komponentów jest bardziej czytelne a dystrybucja naszych bibliotek z komponentami UI to bułka z masłem. Jeśli jesteście w stanie zaakceptować dodatkowy narzut przy parsowaniu — myślę, że warto dać tej technologii szansę.
Zdjęcie główne artykułu pochodzi z stocksnap.io.