23.04.20187 min

Maciej MatuszewskiFront-end developer EL Passion

? Styled-Components: czy warto je wypróbować?

Czyli alternatywny sposób nadawania stylów aplikacji React

? Styled-Components: czy warto je wypróbować?

Rozpoczynałem ostatnio nowy projekt i w którymś momencie musiałem zastanowić się nad podejściem do nadawania stylów. Pewnie, mogłem korzystać z tradycyjnego BEM, ale skłaniałem się ku modułom CSS, które dobrze sprawdzają się z Reactem. Siadałem już do pracy, gdy współpracownik zapytał, czy myślałem o bibliotece styled-components.

Początkowo podszedłem do tego sceptycznie. Na ogół pisanie stylów w JS nie przychodzi mi swobodnie, jednak czasem trzeba spróbować czegoś nowego.  

Styled-components są popularne na GitHubie (w momencie pisania: 15.5k) i w niektórych projektach używane na produkcji. Postanowiłem spróbować, nie zastanawiając się dłużej. 

Czym są styled-components?

Załóżmy, że stawiasz pierwsze kroki w CSS i słyszałeś o nowej, popularnej bibliotece - styled-components. Po krótkim researchu dowiesz się, że to jeszcze jedno podejście do stylowania w CSS w JavaScript. Biblioteka ta powstała z myślą o React, ale pomysł znalazł zastosowanie - choć już nie tak popularne - w vue.js, React Native, a nawet w tradycyjnym DOM.

Możemy już przejść do sedna: czym są styled-components? Jakim trzem największym problemom próbują zaradzić?


Tworzenie pierwszego komponentu

Styled-components są generowane w locie, dlatego nie musimy modyfikować konfiguracji używanego przez nas bundlera. Yeah! Wystarczy, że uruchomimy npm install styled-components i zaimportujemy do komponentu. Przyjrzyjmy się temu na rzeczywistym przykładzie:

import styled from 'styled-components';

const HeaderText = styled.h1`
  font-size: ${ props => props.large ? 32 : 24 }px;
  color: wheat;
  text-transform: uppercase;
`;

export default () => (
  <HeaderText large>Looks Cool</HeaderText>
);

Podstawowe zastosowanie styled-components


Stworzyliśmy tutaj komponent, który renderuje HeaderText - stylowany element h1. Jeśli przekażemy props large, przycisk zostanie wyrenderowany z większym rozmiarem czcionki. Standardowo - np. w BEM - musielibyśmy napisać dwie klasy i zastosować je warunkowo.

Spójrzmy na ten sam przypadek, ale w tradycyjnym podejściu:

.header__text {
  font-size: 24px;
  color: wheat;
  text-transform: uppercase;
  
  &--large {
    font-size: 32px;
  }
}

Stylowanie dla komponentu HeaderText

import './HeaderText.scss';

export default ({ 
  large,
  children,
  className,
  ...props
}) => {
  const classList = `header__text ${large && 'header__text--large'} ${className}`;
  
  return (
    <h1 className={classList} {...props}>{children}</h1>
  );
};


Drugi przykład jest oczywiście bardziej złożony. Musimy pamiętać o przekazywaniu propa className, ale również wszystkich innych - jak propsów onClick, czy innych zdarzeń.

Cała funkcjonalność styled-components opiera się na raczej niepopularnej i mało znanej funkcji ES6 - Tagged Template Literals. Aby zademonstrować ich działanie, stworzymy prostą funkcję log. Zaloguje ona wszystkie przekazane argumenty do konsoli i użyje ich w dwóch różnych scenariuszach.

const log = (...args) => console.log(...args);

log('This', 'is', 'useless');
// 'This', 'is', 'useless'

const name = 'Bob';
log`Hi ${name}! How are you?`;
// ['Hi ', '! How are you?'], 'Bob'

Funkcja, która loguje wszystkie przekazane do niej propsy


Jeżeli wywołamy ją jako standardową funkcję, po prostu wyrzuci wszystkie przekazane do niej argumenty, jeden po drugim. Natomiast jeżeli wywołamy tę funkcję używając tagged template literals to jako pierwszą wartość otrzymamy tablicę wszystkich ciągów znaków, które były w literale szablonu. Następnie zwrócone zostaną wszystkie zmienne i funkcje, które przekazaliśmy do interpolacji.

Jeśli chcesz wiedzieć więcej o Tagged Template Literals, sprawdź dokumentację MDN lub przeczytaj artykuł napisany przez jednego z twórców biblioteki styled-components, Maxa Stoibera.

Jakie problemy rozwiązują styled-components?


Enkapsulacja stylów

W przeszłości (znowu nie tak dalekiej, ale hej - mówimy o JS…), aby utrzymać przejrzyste i gotowe do wielokrotnego użytku style, trzeba było skorzystać z jednej z popularnych metodyk - na czele z BEM - ale mieliśmy też OOCSS, SMACSS, a nawet Atomic CSS. Wszystkie wciąż są trafne i niezwykle przydatne, ale pojawiły się nowe możliwości rozwiązania kwestii zasięgu stylów.

Moduły CSS pozwalają na import plików CSS lub SCSS do JS i odwoływanie się do nazw klas jako kluczy w zaimportowanym obiekcie. To może wydawać się skomplikowane, ale tak naprawdę sprowadza się do łączenia naszych prostych nazw klas - jak .button - z jakimś losowym członem - np. .button_sRgs42. Dzięki temu możemy korzystać z dobrodziejstw lokalnego zasięgu. Styled-components generują też losowe stringi dla nazw klas, ale potęga tkwi w tym, że dzieje się to automagicznie, bez potrzeby importowania arkuszy stylów - co przywodzi na myśl drugi problem.


Łączenie stylów i komponentów

W przypadku styled-components kiedy chcemy ostylować element najpierw go tworzymy, a później stylujemy. Biblioteka tworzy automatycznie losowy ciąg znaków i przypisuje wygenerowaną nazwę klasy do Reacta, vue.js, DOM lub innego komponentu.  


Dynamiczne zmiany stylów

Jako że piszemy nasze style w JavaScript możemy je dynamicznie zmieniać. Zatem, zamiast tworzyć dwie różne klasy dla przycisków .default i .disabled, możemy zastosować w naszych stylach operator warunkowy, który będzie wyglądał mniej więcej tak: color: ${disabled ? 'grey' : 'black'};.

${} jest jeszcze jedną funkcjonalnością ES6, nazywaną interpolacją stringów. Możesz dowiedzieć się o niej więcej na MDN.

Za kulisami

Przyjrzymy się wewnętrznej logice styled-components, aby lepiej zrozumieć z czym tak naprawdę mamy do czynienia. Co się dzieje, gdy tworzymy ostylowany komponent? Przejdźmy przez to krok po kroku.

const Button = styled.button`
  font-size: 12px;
  background-color: ${props => props.solid ? 'red' : 'blue' };
`;


Na początku określamy, że chcemy stworzyć komponent z elementu button. Styled-components tworzą nowy komponent React. Gdy zostanie wywołana funkcja componentWillMount zweryfikuje ona czy komponent zmieni się dynamicznie, bazując na props. Jeżeli nie, silnik nie będzie próbował ponownie wygenerować stylów. Podobnie dzieje się w przypadku componentWillReceiveProps. Jeżeli następuje dynamiczna zmiana, silnik ponownie wylicza style przez wywołanie funkcji setState, w której wykonywane są potrzebne wyliczenia. Kiedy zostanie wywołane componentWillUnmount nastąpi zwykłe odpięcie obserwatorów, by nie przeliczać nieużywanych stylów.

Kiedy renderujemy komponent silnik bierze tablicę stringów i wszystkie argumenty zwrócone z tagged template literal. Pozostając przy powyższym przykładzie z komponentem button, argumenty będą wyglądały następująco:

(
  [ 'font-size: 12px ;background-color: ', ';' ], // first argument
  (props => props.solid ? 'red' : 'blue')
)


Jak zauważyliśmy wcześniej, pierwszy argument jest tablicą wszystkich stringów w nawiasach klamrowych, a następne argumenty (w powyższym przykładzie jest tylko jeden) są zmiennymi/funkcjami przekazanymi do interpolacji.

Wtedy komponent sprawdza, czy któryś z argumentów jest funkcją. Jeśli tak, stosuje tę funkcję do elementu z tablicy z odpowiednim id (pierwsza funkcja odnosi się do elementu z indeksem 0). Następnie przekazuje props z komponentu do tej funkcji jako argumenty. Dołącza wynik funkcji do wybranego stringa. Na koniec łączy wszystkie stringi i wstrzykuje je do tagu head na naszej stronie.

'font-size: 12px; background-color: ' + 'red' + ';'


To duże uproszczenie, ale pokazuje główny mechanizm stojący za styled-components. Pozwala na pisanie prawdziwego CSS i wspiera 100% jego funkcjonalności, co nie pojawiało się wcześniej (lub nie było na tyle popularne).

Zamieszczam poniżej krótki przykład, który odwzorowuje zachowanie styled-components (bez faktycznego wstrzykiwania stylów; tworzę tylko string CSS).

// component props mock
const properties = {
  color: 'red',
  large: true
};

// curried function
const styled = (props) => (strings, ...args) => {
  return strings.map((string, i) => {
    const correspondingItem = args[i];
  
    switch(typeof correspondingItem) {
      case 'function': 
        return string + correspondingItem(props);
      case 'undefined':
        return string;
      default:
        return string + correspondingItem;
    }
  }).join('');
}

const styles = styled(properties)`
  width: 10px;
  height: 10px;
  background-color: ${props => props.color};
  font-size: ${props => props.large ? 20 : 14}px;
  color: white;
`;

console.log(styles);
/*
"
  width: 10px;
  height: 10px;
  background-color: red;
  font-size: 20px;
  color: white;
"
*/


Wstrzykiwanie stylów

Od wersji 3.1.0 wypuszczonej pod koniec stycznia 2018, styled-components korzystają na produkcji z mniej znanego insertRule API. Ta mała zmiana pozwala na skrócenie czasu ładowania strony (do stanu interaktywnego - Time-To-First-Interactive) w większości aplikacji. W testach obciążeniowych biblioteki (co nie jest implementacją jaką spotykamy na co dzień) poskutkowało to 10x-20x szybszym ładowaniem. Dzięki usunięciu tego wąskiego gardła styled-components zaczynają pasować również do większych aplikacji, a nie, jak do tej pory, tylko do pobocznych projektów.

Narzędzia

Powstało kilka narzędzi, które mają ułatwić używanie styled-components, a tym najbardziej przydatnym jak do tej pory jest babel plugin. Umożliwia stworzenie konfiguracji, która naprawdę pomaga w dewelopmencie (jak np. server side rendering) i umożliwia zastosowanie ładnych nazw klas, które ułatwiają debugowanie. Dzięki niemu możemy określić też opcje do preprocessingu i minifikacji.

Problemy

Styled-components nie są (jeszcze) bezbłędne - napotkałem wiele mniejszych i większych problemów. Niektóre z nich wkrótce zostaną rozwiązane, bo projekt ma wielu współautorów i aktywnie się rozwija. Oto, co powinieneś wiedzieć zanim rozpoczniesz pracę ze styled-components.

  • Występują problemy z wydajnością, które trzeba zrozumieć, aby uniknąć niepotrzebnego ponownego renderowania komponentów i słabego działania animacji. Zniwelowano je częściowo dzięki niedawnemu wprowadzeniu insertRule API.
  • Jeśli dzieje się coś nie tak ze styled-components, zawiesza się cała aplikacja.
  • Styled-components nie można wyłączyć do statycznego pliku CSS.
  • Linting ma sporo błędów, np. są problemy z lintowaniem spacji, jeżeli występują jakiekolwiek reguły, które specyfikują ich ilość przy używaniu bloków warunkowych stylów.
  • Ścisłe powiązanie z Reactem. Migracja z obecnie używanego frameworka lub ponowne użycie istniejącego komponentu w innym projekcie jest trudne.
  • To nowa technologia bez jasno zdefiniowanej metodyki i strategii.


Moja opinia

Po miesiącu korzystania ze styled-components mogę stwierdzić, że nie jestem jeszcze do końca przekonany co do ich przewagi nad tworzeniem arkuszy stylów, ale zauważam olbrzymi potencjał. Nawet jeśli używanie ich w aplikacjach webowych wydaje się trochę eksperymentowaniem, świetnie pracowało mi się z nimi w projekcie React Native. Sprawdzają się o wiele lepiej niż pisanie CSS jako obiektów JavaScript.

Myślę, że - pomimo kilku niedociągnięć - styled-components to świetna biblioteka i kolejny krok w ewolucji współczesnego CSS. Wyrazy uznania dla Glena Madderna i Maxa Stoibera - jej twórców.

Chcesz dowiedzieć się więcej? Dołączam kilka artykułów i źródeł, którymi wspierałem się pisząc ten artykuł i gorąco zachęcam do ich przejrzenia.


Artykuł w wersji angielskiej do przeczytania na blogu EL Passion.

<p>Loading...</p>