Poznaj 5 Sposobów Stylowania Komponentów React w 2020

Zdjęcie autorstwa Markus Spiske temporausch.com z Pexels

Główną składową Reacta są komponenty – bardzo popularnym podejściem jest enkapsulacja stylów właśnie w ramach komponentów. Stylowanie to istotna część tworzenia interfejsu użytkownika, można do niego podejść na wiele sposobów – w tym poście przedstawię 5 które są najbardziej warte uwagi w 2020 roku.
Wybór oczywiście jest subiektywny – tak jak subiektywne odczucia zdecydują o tym, który z opisanych sposobów jest najlepszy w Twoim przypadku.


Sposoby

  1. Metodologia BEM
  2. Styled-components
  3. CSS Modules
  4. Shadow DOM
  5. Stylable

BEM

Podejście Block – Element – Modifier ustrukturyzowało nazewnictwo klas wprowadzając podział na klasy blokowe – będące klasą główną komponentu, klasy modyfikujące – nadpisujące konkretne style klasy bazowej oraz klasy elementów – znajdujące się wewnątrz bloku. Elementy również mogą posiadać modyfikator.
BEM tworzy z pozoru koszmarnie wyglądające potworki:

<div className="form__element form__element--active" >...</div>

Jak jednak przekonasz się niebawem, dzięki swej prostocie i jednoznacznej strukturze jest to jedna z najlepszych metodologii pisania stylów. Poznajmy więc konkretne składowe

Block

Klasa blokowa jest klasą główną komponentu, najczęściej oznaczony nią jest najbardziej zewnętrzny element komponentu:

<form className="form">...</form>

Z punktu widzenia Reactowego można przyjąć uproszczenie, że nazwa bloku jest taka sama jak nazwa komponentu. Od nazwy bloku będą pochodzić kolejne składowe, czyli elementy i modyfikatory.

Element

Element jest częścią bloku, która poza nim nie ma sensu – przykładem może być element w którym zawiera się pole formularza. Nazwę klasy tworzymy poprzez dodanie do klasy bloku podwójnego podkreślenia __ i nazwy bloku:

<div className="form__group">...</>

Style zawarte w klasie elementu mogą zawierać np odstępy pomiędzy elementami formularza – modelowym przykładem, choć nie napisanym w metodologii BEM, jest klasa form-group z Bootstrapa. Klasa ta określa, że pomiędzy polami ma być 1rem marginesu:

Elementem może być np pole w formularzu:

<input className="form__input" />

W trakcie prac może pojawić się sytuacja, że elementu danego bloku składają się w jedną całość i można wydzielić z nich osobny komponent. Przykładowo, jeżeli mielibyśmy:

<div class="form__group">
    <label className="form__label" htmlFor="name">Label</label>
    <input className="form__input" id="name" type="text" />
</div>

Możnaby zastanowić się nad wydzieleniem osobnego komponetu np form-element:

<div className="form-element">
    <label className="form-element__label" htmlFor={inputId}>
    <input className="form-element__input" id={id} type={type} onChange={onChange} />
</div>

I użyciem go w formularzu:

<form className="form">
    <FormElement inputId="name" type="text" onChange={onChange} />
</form>

Modifier

Modyfikatory służą tworzeniu różnych wariantów danego bloku lub elementu – na przykład ze względu na typ lub stan. Tworzymy go poprzez dopisanie do modyfikowanego elementu dwóch minusów — i nazwy modyfikatora:

.form__group {
color: black;

&--error {
color: red
}
}

Operator ampersand z SCSS

W SCSS możemy użyć operatora ampersand (&) który jest skróconą formą wklejania selektora rodzica. Przykładowo kod

.user {
  &.login {
    background: none;
  }
}

Zostanie przetransformowany do

.user.login {
  
}

Innymi słowy & zostanie zastąpiony tym, co zostało wpisane w selektorze poziom wyżej.
Umożliwia nam to uproszczony zapis klas BEM, czyli zamiast:

.form-element {
  margin: 16px;
}

.form-element__label {
  font-sze: 16px;
}

Możemy skrócić zapis do:

.form-element {
  margin: 16px;

  &__label {
    font-size: 16px;
  }
}

Zapis ten powoduje że kod jest bardziej zwarty oraz że nie popełnimy literówki wpisując nazwę komponentu N razy w kodzie CSS.


Styled-components

Styled-components to biblioteka, pozwalająca na pisanie styli niejako inline w komponentach – podejście to diametralnie różni się od BEMa.
Za jej pomocą tworzymy tylko komponenty widokowe, ze stylami CSS – znacząco pomaga to w odseparowaniu logiki od prezentacji.
Komponent tworzymy poprzez styled.[element HTML].`style CSS`:

const Button = styled.button`
    background-color: white;
    padding: 6px 10px;
`
...
<Button>Send</Button

Z łatwością możemy zmieniać style komponentu za pomocą parametrów:

const Button = styled.button`
    background-color: white;
    padding: 6px 10px;

    ${props => props.primary && css`
        background-color: 'blue';
    `}
`
...
<Button primary>Send</Button

Jak łatwo zauważyć parametryzowanie odbywa się za pomocą funkcji, możemy więc utworzyć osobną funkcję która obsłuży więcej przypadków:

const getModifierStyle = props => {
    if (props.primary) {
        return css`background-color: blue`;
    }
    if (props.success) {
        return css`background-color: green`;
    } 
}

const Button = styled.button`
    background-color: white;
    padding: 6px 10px;

    ${getModifierStyle}
`
...
<Button primary>Send</Button

<Button success>Save</Button 

Theming- skórki

Jedną z najciekawszych funkcjonalności styled-components jest Theming, czyli popularnie mówiąc Skórki. Dzięki skórkom możemy łatwiej utrzymać spójność wyglądu w naszej aplikacji:

const Button = styled.button`
    background-color: ${props => props.theme.backgroundColor};
    color: ${props => props.theme.textColor};
    padding: 6px 10px;
`
... 

const theme = {
    backgroundColor: 'white';
    textColor: 'black';
}
...
<ThemeProvider theme={theme}>
    <Button>
</ThemeProvider>

Theming – przełączanie pomiędzy skórkami

Ponieważ skórka jest przekazywana do ThemeProvider jako parametr, możliwość przełączania pomiędzy skórkami jest bardzo prosta w implementacji. Załóżmy, że chcemy dać użytkownikowi możliwość przełączania się pomiędzy skórką zwykłą a ciemną:

const Button = styled.button`
    background-color: ${props => props.theme.backgroundColor};
    color: ${props => props.theme.textColor};
    padding: 6px 10px;
`
... 

const lightTheme = {
    backgroundColor: 'white';
    textColor: 'black';
}

const darkTheme = {
    backgroundColor: 'black';
    textColor: 'white';
}
...

// funkcja setTheme może być podpięta np do przycisku
const [theme, setTheme] = useState('light'); 

<ThemeProvider theme={ theme === 'light'? lightTheme : darkTheme}>
    <Button>
</ThemeProvider> 

CSS Modules

Czym są moduły CSS

Możemy przyjąć, że poprzez moduł CSS rozumieć powinniśmy plik CSS, w którym wszystkie nazwy klas oraz animacji są unikalne w skali aplikacji. Sama składnia CSS pozostaje taka sama:

.group {
  margin: 20px;
}

.label {
  font-size: 14px;
  font-weight: 300;
}

.input {
  padding: 10px;
  border: none;
}

Zmienia się natomiast sposób użycia w komponencie Reactowym:

import styles from "./FormElement.scss";

export const FormElement = () {
  return (
    <div className={styles.group}>
      <label className={styles.label}>Name</label>
      <input className={styles.input} />
    </div>
  );
}

Zamiast przypisywać elementowi klasę „z palca”, podajemy wartość klucza z elementu styles. CSS Modules pozwala zaimportować plik CSS jako obiekt z klasami CSS jako kluczami. Wartościami są nowe, zmodyfikowane nazwy klas.
Efekt jaki ujrzymy w przeglądarce powinien być podobny do tego:

<div class="formElement__group__1esd3">
  <label class="formElement__label__1esd3">Name</label>
  <input class="formElement__input__1esd3" />
</div> 

Do nazw klas z pliku CSS doklejona została nazwa komponentu jako prefiks oraz unikalny ciąg znaków jako sufiks bo to, by zapewnić unikalność tychże nazw.
Pamiętać musimy o tym, by nazwy klas pisać z użyciem notacji camelCase. Dzięki temu do kluczy obiektu styles będziemy mogli odnosić się notacją z kropką np styles.formGroup. Notacja z kropką nie jest możliwa dla standardowego sposobu zapisu klas z użyciem myślników, musimy użyć notacji z nawiasami kwadratowymi – styles['form-group'], co nie należy do przyjemnych.

React CSS Modules

Choć nowoczesne IDE potrafią podpowiadać klucze obiektu styles, nie każdemu musi podobać się niemożność wpisania klasy bezpośrednio, używanie notacji camelCase do nazw klas również nie jest czymś do czego na frontendzie przywykliśmy. Powstała więc biblioteka React CSS Modules, dzięki której komponent FormElement będzie wyglądał następująco:

import styles from "./FormElement.scss";
import CSSModules from 'react-css-modules';

const FormElement = () {
  return (
    <div styleName="group">
      <label styleName="label">Name</label>
      <input styleName="input" />
    </div>
  );
}

export default CSSModules(FormElement, styles);

Gdzie zamiast do parametru className, nazwę klasy przekazujemy do parametru styleName. Ponieważ nie używamy kluczy jak w czystym CSS Modules, możemy używać zwyczajnej notacji dla klas CSS, przykładowo styleName="form-group".
Dodatkowym plusem tej biblioteki jest to, że nadal możemy dodawać klasy za pomocą className. Sugeruje naturalny podział – do className trafiają style globalne np klasy z frameworków, a do styleName style dodatkowe, specyficzne dla komponentu.

Wsparcie CSS Modules w create-react-app

Dobra wiadomość – create-react-app wspiera CSS modules bez konieczności żadnej konfiguracji. Spójrzmy na przykład bez CSS Modules:

import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">
      Test app
    </div>
  );
}

export default App;

Oraz na kod wynikowy z przeglądarki:

Przepiszmy teraz przykład tak, by był zgodny z konwencją CSS Modules:

import React from 'react';
import styles from  './App.module.css';

function App() {
  return (
    <div className={styles.App}>
      Test app
    </div>
  );
}

export default App;

Oraz na kod wynikowy z przeglądarki:

Widzimy, że nazwa klasy zmieniona wg reguł CSS Modules.

babel-plugin-react-css-modules

Na stronie biblioteki React CSS Modules znajdziemy informację, by sprawdzić czy plugin do Babela babel-plugin-react-css-modules odpowiada naszym potrzebom, jako szybsza i mniejsza alternatywa dla React CSS Modules.
Aby użyć pluginu w aplikacji stworzonej przy pomocy create-react-app musimy uzyskać dostęp do konfiguracji – co niestety wymusza uruchomienie polecenia npm run eject . Po uruchomieniu tej komendy pliku package.json pojawi się ogromna liczba nowych wierszy. Pierwszym krokiem jest instalacja pluginu przez npm install babel-plugin-react-css-modules --save należy odpowiednio skonfigurować Babela w package.json:

"babel": {
"presets": [
"react-app"
],
"plugins": [
"babel-plugin-react-css-modules"
]
}

Następnie w pliku webpack.config.js należy ustawić wzorzec tworzenia nazw klas:

const localIdentName = "[path]___[name]__[local]___[hash:base64:5]";

Trzecim krokiem jest zmodyfikowanie ustawień loadera stylów CSS by korzystał z wzorca:

use: getStyleLoaders({
  importLoaders: 1,
  sourceMap: isEnvProduction && shouldUseSourceMap,
  modules: {
    mode: "local",
    localIdentName
  }
}),

Ostatnim, czwartym elementem układanki jest wyłączenie wsparcia CSS Modules, ponieważ gryzie się to z pluginem. Kod który należy zakomentować:

{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},

W innych wersjach create-react-app ten kod może być inny – ważne by wyłączyć CSS Modules.

Rozwiązanie to jest mocno inspirowane udzieloną mi na StackOverflow odpowiedzią: babel-plugin-react-css-modules does not change class name in style tag.


Shadow DOM – React Shadow

Czym w ogóle jest Shadow DOM

Najogólniej, Shadow DOM to wyizolowane drzewo DOM z kodem HTML ukryte wewnątrz taga HTML. Takie drzewo DOM ma na przykład tag textarea – przykładowo jeżeli w Chrome Dev Tools w ustawieniach zaznaczymy opcję „show user agent shadow DOM”:

To po wybraniu w Dev Tools elementu textarea możemy zobaczyć następujący kod:

Dokonując więc pewnego uproszczenia, możemy założyć że w Chrome tag textarea jest zaimplementowany jako div.

Przykład

Załóżmy że mamy następujący kod HTML:

<style>
.paragraph {
color: red;
}
</style>
<p class="paragraph" id="no-shadow">
No shadow
</p>
<div id="with-shadow"></div>

Klasa paragraph przestawia kolor tekstu na czerwony. Mamy również element div, do którego dokleimy Shadow DOM:

const div = document.querySelector('#with-shadow');
const shadowRoot = div.attachShadow({mode: 'open'});
const style = `
<style>
.paragraph { color: green; }
</style>`;
const html = '<p class="paragraph">Has shadow</p>';
shadowRoot.innerHTML = `${style}${html}`;

Kod wynikowy w przeglądarce pokazuje, że element z klasą paragraph wewnątrz Shadow DOM nie ma stylów z klasy w głównym dokumencie:

Dziedziczenie stylów z Shadow Host

Co jednak w sytuacji, gdy div do którego podpinamy Shadow DOM miałby jakieś style? Dodajmy mu klasę paragraph:

Pozornie wygląda to na dziedziczenie po klasie paragraph – tak naprawdę jednak, jest to dziedziczenie po Shadow Host – czyli elemencie, do którego podpinamy Shadow DOM. W naszym przypadku Hostem jest div, któremu klasa paragraph przestawia kolor na czerwony. Klasa paragraph sama w sobie nie ma wpływu na style wewnątrz Shadow DOM.

React Shadow

React Shadow jest biblioteką znacznie upraszczającą tworzenie Shadow DOM dla poszczególnych komponentów Reactowych. Na stronie biblioteki jest zawarta informacja by importować style jako zwykły string, co nie dzieje się w przypadku aplikacji utworzonej przez create-react-app. Dla uproszczenia, przyjmijmy więc taki kod jako przykład dla aplikacji CRA:

import React from 'react';
import root from 'react-shadow';
const styles = `
.App {
text-align: center;
color: red;
padding: 300px;
}
`;

function App() {
return (
<root.div>
<style>{styles}</style>
<div className="App">
Test
</div>
</root.div>
);
}

export default App;

Efekt w przeglądarce jest następujący:


Stylable

Stylable byłaby kolejną biblioteką zapewniającą unikalność nazw klas gdyby nie jej jedna unikatowa cecha – pozwala, z poziomu komponentu, stylować komponenty-dzieci. Komponent może wystawić API z którego skorzystać może rodzic – dzięki czemu nie trzeba dorabiać modyfikatora dla każdego kolejnego wariantu stylu, oraz nie trzeba przyjmować wielu parametrów odpowiedzialnych za stylowanie poszczególnych części komponentu. Brzmi dziwnie – czas więc na przykład 🙂

Najpierw utwórzmy aplikację zawierającą Stylable przez sforkowaną wersję create-react-app poleceniem:

npx create-react-app@3.2.0 --scripts-version @stylable/react-scripts

Uruchomienie polecenia npm start odpali stronę startową.

Jednym z komponentów w aplikacji jest header.tsx. Dodajmy do niego następujący kod:

<h2 className={classes.test}>Test</h2>

Oraz w stylach w pliku header.st.css:

.test {
  color: white;
}

W DevToolsach zauważymy, że stylowanie zadziałało:

Nazwa klasy w pliku CSS nagłówek jest również slotem, którego komponent zewnętrzny może użyć do ostylowania. Dostęp do slotu z CSS wygląda następująco: NazwaKomponentu::NazwaKlasy. Jeżeli w komponencie Header mamy klasę test, slotem będzie Header::test. Dodajmy do pliku app.st.css następujący kod:

Header::test {
color: red;
}

Po przeładowaniu strony w DevToolsach widzimy, że styl został nadpisany:


Podsumowanie

Podczas wyboru sposobu stylowania należy wziąć pod uwagę wiele czynników – czas który można poświęcić na research i sprawdzenie różnych rozwiązań, wielkość projektu, poprzednie doświadczenia. Czasami warto wybrać rozwiązanie które, choć początkowo skomplikowane, da wartość w późniejszym okresie życia projektu. Innym razem warto postawić na prostotę i możliwość szybkiego postępu prac. Mam nadzieję, że ten post choć trochę ułatwi Wam dokonywanie wyborów.