Sposobów i podejść do nadawania stylów komponentom React jest całe mnóstwo. Sam jakiś czas temu pisałem o jednym z nich - styled components. Dziś przedstawię kolejny z nich - CSS Modules - z którym zetknąłem się, poniekąd z przymusu, podczas pracy nad moim projektem Polski Frontend. Myślę, że wspominałem już, że do budowy front-endu do tego projektu wykorzystałem starter react-starter-kit - otóż miał on już wszystko skonfigurowane pod CSS Modules, postanowiłem więc dać temu szansę.

No, a dziś przyszedł czas na przedstawienie z czym to się je… Zapraszam do lektury!

CSS Modules - o co chodzi?

Myślę, że nie będę się tutaj wdawał w szczegóły konfiguracyjne bo w przypadku Reacta nie ma wielkiej filozofii. Każdy, jeśli tylko będzie chciał, znajdzie wszystko co trzeba w dokumentacji projektu. Teraz wystarczy jeśli napiszę, że opcja modułów CSS jest wbudowana w css-loader dla webpacka (wystarczy włączyć flagę modules przy konfiguracji loadera). W końcu piszę to w kontekście Reacta, a w aplikacjach tworzonych przy użyciu tej biblioteki webpack jest na porządku dziennym. Nie znaczy to oczywiście, że tylko z Reactem można go używać - i o tym właśnie więcej we wspomnianej dokumentacji.

Ok, skoro kwestie instalacyjno-konfiguracyjne mamy omówione, przejdźmy do ogólnej idei CSS Modules. Jak zawsze, najlepiej będzie zacząć od przykładu! Najpierw spójrz na plik CSS (LoginForm.css):

.login {
  background: #fff;
}

.input {
  border: 1px solid #000;
}

.button {
  color: #000;
  background: #ff0000;
}

Domyślasz się już zapewne, że style te dotyczą komponentu formularza logowania. Spójrzmy więc teraz na ten komponent:

import React from 'react';
import styles from './LoginForm.css';

class LoginForm extends React.Component {
  render() {
    <form className={styles.login}>
      <input className={styles.input} type="text" />
      <input className={styles.input} type="password" />
      <button className={styles.button} type="submit">Submit</button>
    </form>
  }
}

export default LoginForm;

Pierwsze, na co chciałbym, abyś zwrócił uwagę jest import z pliku LoginForm.css. Plik ten jest tutaj traktowany jako pełnoprawny moduł JavaScript, który przypisujemy do zmiennej styles.

Kolejna interesująca rzecz dzieje się w metodzie render komponentu LoginForm. Zwróć przede wszystkim uwagę na atrybuty className poszczególnych elementów JSX oraz sposób w jaki wykorzystano tam zmienną styles!

To właśnie jest “magia” modułów CSS - dzięki ich zastosowaniu, mamy możliwość traktować pliki CSS jak moduły JavaScript, a poszczególne klasy zdefiniowane w tym pliku jak właściwości obiektu.

Wyrenderowany HTML

Warto jeszcze spojrzeć na kod HTML będący wynikiem renderowania powyższego komponentu:

<form class="loginForm__login___1g3fw">
  <input class="loginForm__input___w43a1" type="text" />
  <input class="loginForm__input___w43a1" type="password" />
  <button class="loginForm__button___2a1sh" type="submit">Submit</button>
</form>

Hmm… W sumie nawet trochę to przypomina BEM, prawda? Ok, to “rozkmińmy” co oznaczając poszczególne człony powyższych, wygenerowanych klas CSS…

Najpierw mamy loginForm co odpowiada nazwie całego komponentu. Czyli w sumie blok z BEM, prawda? Następnie mamy nazwę klasy z pliku CSS czyli element. Na końcu natomiast widać jakiś losowy ciąg znaków…

Te znaki na końcu mają zapobiegać ewentualnym konfliktom nazw. Chodzi o to, abyśmy w wielu różnych komponentach mogli zdefiniować taką samą nazwę klasy, powiedzmy “item”. W każdym z tych komponentów może ona przecież znaczyć coś zupełnie innego i mieć zupełnie inne style.

Zady i walety (według mnie)

Na początek kilka negatywnych odczuć jaki mi się nasunęły podczas pracy z modułami CSS:

  • po pierwsze, według mnie, odwoływanie się do każdego ze stylów poprzez obiekt jest mniej wygodne niż zwykłe podawanie nazw klas jako ciąg znaków
  • poza tym, jesteśmy przez powyższe zmuszeni do używania notacji “camelCase” dla nazw klas CSS, co trochę kłóci się z ogólnie przyjętymi obecnie praktykami (ewentualnie można robić tak: styles['class-name'] ale to trochę karkołomne)
  • do tego dochodzą jeszcze problemy z łączeniem modułów CSS ze stylami globalnymi, które nie są modułami czy też ze stylami z zewnętrznych bibliotek (np. Bootstrap czy FontAwesome) - teoretycznie zakłada się, że przy CSS Modules w ogólnie nie powinno być stylów globalnych ale jak to w życiu, to nie jest to aż takie proste do osiągnięcia…

Z drugiej strony podejście to ma też pewne zalety:

  • dzięki modułom CSS da radę (przynajmniej w WebStorm) korzystać z podpowiedzi dostępnych w danym module klas
  • na pewno też podejście to pomaga to w enkapsulacji stylów w komponencie - dany styl musi znajdować się w zaimportowanym pliku CSS aby można go było użyć

Ostatnie co napisałem (o enkapsulacji), to zdaje się, główna motywacja do stworzenia tego podejścia. Faktycznie nawet się to sprawdza - pamiętam, że kiedy zaczynałem z Reactem to często zdarzało mi się stworzyć komponent wraz ze stylami do niego. Później, w miarę jak komponent rósł, wydzielałem z niego mniejsze komponenty. Niby wszystko fajnie tyle, że zdarzało się, czy to z lenistwa, czy z braku czasu, że wszystkie style zostawały w komponencie “bazowym”…

Jeśli używasz CSS Modules to jesteś przed takim czymś zabezpieczony - jeśli przeniesiesz tylko kod, a style zostaną w starym komponencie to po prostu przestaną one działać. Trzeba więc przenosić wszystko razem, dzięki czemu trochę trudniej o bałagan.

React CSS Modules

Powyżej wymieniłem kilka negatywnych odczuć jakie mi się nasunęły podczas korzystania z CSS Modules w React. Okazuje się, że można im wszystkim łatwo zaradzić dzięki bibliotece react-css-modules!

W myśl zasady, że 16 linii kodu jest więcej warte niż 1024 słowa (no dobra, suchar - przepraszam), spójrzmy na lekko przerobiony komponent LoginForm z poprzedniego przykładu (bibliotekę react-css-modules możesz w sposób standardowy zainstalować z npm):

import React from 'react';
import styles from './LoginForm.css';
import CSSModules from 'react-css-modules';

class LoginForm extends React.Component {
  render() {
    <form styleName="login">
      <input styleName="input" type="text" />
      <input styleName="input" type="password" />
      <button styleName="button" type="submit">Submit</button>
    </form>
  }
}

export default CSSModules(LoginForm, styles);

No dobra to teraz spójrzmy co się zmieniło. Pierwsza rzecz to doszedł import z pakietu react-css-modules (użyjemy go na końcu).

To co bardziej rzuca się w oczy to to, że tym razem do klas zdefiniowanych w pliku CSS nie odnosimy się poprzez obiekt styles! Zamiast tego zwyczajnie używamy ich nazw jako ciągów znaków (czyli nie musimy mieć nazw w notacji “camelCase”).

Jeżeli jesteś spostrzegawczy to zapewne rzuciła Ci się w oczy jeszcze jedna istotna zmiana - otóż zamiast standardowego atrybutu className do ustawiania klas użyto atrybutu styleName. Dzięki zastosowaniu odrębnego, dedykowanego atrybutu uzyskano dodatkowo fajny efekt uboczny:

<div className="global-style" styleName="module-style">...</div>

W ten sposób łatwo odróżnić, które style są globalne, a które należą do danego komponentu. Poza tym nie trzeba kombinować w przypadku kiedy chcemy użyć jednocześnie stylów globalnych i tych modułowych (bez tej biblioteki trzeba korzystać z wstrzykiwania parametrów do ciągów znaków: className={global-style ${style.someClass}}).

Na koniec zwróć jeszcze uwagę na eksport klasy komponentu. Jak widzisz użyto tam metody CSSModule zaimportowanej wcześniej z modułu react-css-modules. Wiąże ona komponent z modułem stylów i zwraca odpowiednio przetworzony komponent wynikowy.

Podsumowanie

Po tym jak chwilę popracowałem z CSS Modules w projekcie React, mam teraz dość mieszane uczucia. Z jednej strony, wspomniana enkapsulacja oraz pomaganie w jej pilnowaniu jest na pewno czymś przydatnym.

Z drugiej strony były też elementy, które mnie denerwowały. Wydaje się, że biblioteka taka jak react-css-modules może po części temu zaradzić. Mówiąc jednak szczerze, nie pracowałem z nią jeszcze “produkcyjnie” więc na ten moment są to tylko domysły poparte “researchem”, którego efekt mogłeś przeczytać powyżej. Poza tym jest to jednak swego rodzaju “hack” i na przykład “wrapowanie” każdego jednego komponentu może być upierdliwe szczególnie jak do tego dorzucimy jeszcze inne “wrappery” (choćby connect z Reduxa).

P.S. Jak wspomniałem, sposobów na style w React jest sporo i chyba czas by się przyjrzeć również pozostałym - myślę więc, że w najbliższym czasie będzie więcej wpisów w tym temacie!