Odwrócona dziewczyna w żółtej bluzie, z różowym plecakiem, siedząca na murku

Stylowanie w React – PPwRJS #8

React wywrócił świat front-endu do góry nogami. Pierw za sprawą JSX doszło do zatarcia granic pomiędzy warstwą struktury (HTML), a warstwą zachowania (JS). Nie trzeba było długo czekać, aby ten trend rozprzestrzenił się na warstwę prezentacji. Zamieszanie na linii React-CSS trwa w najlepsze od trzech lat. Najpopularniejsze sposoby na stylowanie aplikacji w ekosystemie Reacta opisałem na przykładzie trackera kryptowalut.

Wszystko zaczęło się od tego slajdu z prezentacji Christophera Chedeau wygłoszonej pod koniec 2014 roku.

„Zwykły” CSS ma swoje problemy. Brak mu podstawowych funkcjonalności takich jak: zmienne, operacje arytmetyczne czy warunki logiczne. Musimy bawić się z prefiksowaniem deklaracji. Aby ustrzec się przed kolizjami nazw selektorów i problemami z ich specyficznością, musimy uciekać się do skomplikowanych metodyk (np. BEM, OOCSS).

Jak słusznie zauważył Christopher, wszystkie z wymienionych bolączek CSS, znikają po przeniesieniu stylowania do JS.

One language to rule them all? Nie do końca. Migracja stylów do JavaScript ma swoje słabe strony. Co najgorsze, są nimi problemy rozwiązane przez CSS lata temu.

Taka niejasna sytuacja daje pole do popisu dla twórców bibliotek. Jednak aby docenić ich wysiłki, pierw spróbujmy stylowania za pomocą czystego JSa.

Style inline

Bez niespodzianek, JS świetnie radzi sobie z tematami zmiennych, warunkowego przypisywania i dynamicznego obliczania wartości stylów.

const Header = ({ cap }) => (
  <div
    style={{
      display: "flex",
  		flexDirection: "column",
  		alignItems: "center",
 			marginTop: 36
    }}
  >
    <h1
      style={{
        fontFamily: '"Trebuchet MS", Helvetica, sans-serif',
      }}
    >
      Crypto Tracker
    </h1>
    <p>Market Cap: {formatAsCurrency(cap)}</p>
  </div>
);

Nazwy właściwości zapisujemy zgodnie z konwencją camelCase. Na pewno kojarzysz tę składnię z DOM API (np. node.style.backgroundImage). Wartości zwykle zapisuje się jako string. Z jednym małym wyjątkiem: wartości liczbowe są interpretowane jako piksele (patrz: marginTop).

Mamy dwie możliwości przypisywania stylów do elementu. Pierwsza to bezpośrednie przypisanie literał obiektu w style (jak wyżej). Druga, zdecydowanie czytelniejsza, to deklaracja obiektowego kontenera i korzystanie z referencji (jak niżej).

const styles = {
  container: {
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    marginTop: 36,
  },
  title: {
    fontWeight: 'bold',
  },
};

const Header = ({ cap }) => (
  <div style={styles.container}>
    <h1 style={styles.title}>Crypto Tracker</h1>
    <p>Market Cap: {formatAsCurrency(cap)}</p>
  </div>
);

Podczas stylowania kolejnych komponentów zauważyłem, że duplikuję sztywne wartości margin. Ten problem w zwykłym CSS rozwiązują klasy. W przypadku stylów inline wyrbnąłem za pomocą pliku z zmiennymi.

// /src/shared/styles/variables.js

const baseMargin = '15%';
const smallMargin = '5%';

export default {
  baseMargin,
  smallMargin,
};

Wartości wpisane na sztywno zastąpiłem refrencjami do tego modułu. W obliczu ewentualnych zmian wystarczy edycja wartości w jednym miejscu.

Niestety, tutaj kończą się silne strony stylów inline. W wstępie pisałem o tym, że style w JS mają problemy, z którymi CSS uporał się dawno temu. Chodzi o brak wsparcia dla pseudo-klas, media queries, keyframes.

Z tego powodu
dokumentacja Reacta
zaznacza, że style inline nie sprawdzają się jako samodzielne rozwiązanie stylowania aplikacji. Czas najwyższy wziąć w obroty wspomniane w wstępie biblioteki.

Tracker kryptowalut wystylizowany z użyciem stylów inline znajdziesz tutaj.

CSS-in-JS

CSS-in-JS to biblioteki rozwijające stylowanie inline. Część z nich radzi sobie z wszystkimi ograniczeniami, które skreśliły czysty JS.

Ja postanowiłem spróbować swoich sił z Radium. Zobaczmy jak się sprawdzi przy dopieszczaniu wyglądu paska wyszukiwania.

const SearchBar = ({ handleChange, searchQuery }) => {
  const styles = {
    container: {
      display: 'flex',
      marginRight: styleVars.baseMargin,
      justifyContent: 'flex-end',
      marginBottom: 24,
      '@media (max-width: 800px)': {
        marginRight: styleVars.smallMargin,
      },
    },
    input: {
      transition: 'all 0.30s ease-in-out',
      outline: 'none',
      padding: '0.5em 0.6em',
      margin: '5px 1px 3px 0px',
      border: '1px solid #DDDDDD',
      borderRadius: '4px',
      ':focus': {
        boxShadow: '0 0 5px rgba(81, 203, 238, 1)',
        border: '1px solid rgba(81, 203, 238, 1)',
      },
    },
  };

  return (
    <div style={styles.container}>
      <input
        style={styles.input}
        value={searchQuery}
        placeholder="Search"
        onChange={handleChange}
      />
    </div>
  );
};

Radium dostarcza wszystko do czego przyzwyczaił nas CSS razem z zaletami stylów inline. Jeżeli sama koncepcja CSS w JS do Ciebie przemawia to będziesz bardziej, niż zadowolony. Pełen opis API znajdziesz tutaj.

Tracker kryptowalut wystylizowany z użyciem Radium znajdziesz tutaj.

CSS Modules

Dobra nowina dla miłośników tradycyjnych rozwiązań: porzucamy style w JS na rzecz arkuszy CSS na sterydach.

Wszystko deklarujemy po staremu w osobnym pliku .css, importujemy jego zawartość do komponentu w obiektowym wrapperze przygotowanym przez CSS Modules i przypisujemy referencje do className.

// Header.css
.header {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 36px;
}

.header__title {
    font-family: "Trebuchet MS", Helvetica, sans-serif;
}

// Header.js
import React from 'react';
import PropTypes from 'prop-types';
import styles from './Header.css';
import formatAsCurrency from '../../shared/utils/helpers';

const Header = ({ cap }) => (
  <div className={styles.header}>
    <h1 className={styles['header__title']}>Crypto Tracker</h1>
    <p>Market Cap: {formatAsCurrency(cap)}</p>
  </div>
);

Czy opakowanie klas w obiekt JS zasługuje na miano „CSS na sterydach”? Oczywiście, że nie. CSS Modules mają do zaoferowania dużo więcej.

Zacznijmy od myśli przewodniej odróżniającą bibliotekę od zwykłego CSS. Jest ona następująca: domyślnie arkusz stylów powinien mieć zakres lokalny. Jakie ma to przełożenie na działanie CSS Modules? W obiektowym wrapperze kryją się dynamiczne, unikalne nazwy klas generowane osobno dla każdego z komponentów. W ten sposób otrzymujemy modularność i zapobiegamy kolizji nazw.

	<h1 class="Header__header__title___18rQm">Crypto Tracker</h1>

Skoro selektory mają zakres lokalny, a każdy komponent ma swój własny arkusz, można pokusić się na odchudzenie nazwy klasy header__title do title. Ja odpuściłem sobie takie ustępstwa, przyzwyczaiłem się do nazewnictwa BEM.

Kolejną silną stroną CSS Modules jest composes.

// CoinListCell.css
	
.cell {
  display: flex;
  align-items: center;
  border-bottom: 2px solid #dedede;
  padding-top: 10px;
  padding-bottom: 10px;
}

.cell--large {
  composes: cell;
  flex: 1 0 25%;
}

.cell--small {
  composes: cell;
  flex: 1 0 10%;
}

.cell--header {
  composes: cell;
  font-weight: 700;
  border-bottom-width: 4px;
}

Dzięki composes unikamy konieczności łączenia kilku klas. Biblioteka zrobi to za nas.

// React
<CoinListCell className={style[cell-large]} />

// Przeglądarka
<div class="CoinListCell__cell--large___3ZBbs CoinListCell__cell___1tTov">... </div>

Zasięg composes nie jest ograniczony do jednego pliku. Możemy importować style z całego projektu.

// helpers.css
/* BASE MARGINS */
.base-margin-right {
  margin-right: 15%;
}

.base-margin-left {
  margin-left: 15%;
}

/* SMALL MARGINS */
.small-margin-right {
  margin-right: 5%;
}

.small-margin-left {
  margin-left: 5%;
}
		
// CoinListRow.css
.row {
  composes: base-margin-left from '../../../shared/styles/helpers.css';
  composes: base-margin-right from '../../../shared/styles/helpers.css';
  display: flex;
  flex-direction: row;
}

CSS modules to świetny przykład synergii pomiędzy tradycyjnym CSSem i odrobiną CSS-in-JS. Mimo to, nie jestem wielkim fanem tej biblioteki. Na chwile obecną wymaga ona edycji podstawowej konfiguracji webpacka w CRA, co zmusza do ejecta. Po drugie, composes nie działa w media queries i pseudo-klasach bez pomocy procesorów CSS.

Tracker kryptowalut wystylizowany z użyciem CSS Modules znajdziesz tutaj.

Gdyby to było wszystko co Reacta ma do zaoferowania, bez wahania wróciłbym do Radium. Na całe szczęście, najlepsze dopiero przed nami.

Styled components

Wracamy do świata CSS-in-JS ale w nieco innej odsłonie. Styled components opierają się przede wszystkim na oznaczonych łańcuchach szablonowych. Dzięki temu korzystamy z „prawdziwego” CSS w JS, co zapewnia bezproblemowy dostęp do wszystkich jego funkcjonalności, w tym problematycznych pseudoklas i media queries.

// SearchBar.js
const Wrapper = styled.div`
  display: flex;
  margin-right: ${styleVars.baseMargin};
  justify-content: flex-end;
  margin-bottom: 24px;

  @media (max-width: 800px) {
    margin-right: ${styleVars.smallMargin};
  }
`;
			
const Input = styled.input`
  transition: all 0.3s ease-in-out;
  outline: none;
  padding: 0.5em 0.6em;
  margin: 5px 1px 3px 0;
  border: 1px solid #dddddd;
  border-radius: 4px;
  &:focus {
    box-shadow: 0 0 5px rgba(81, 203, 238, 1);
    border: 1px solid rgba(81, 203, 238, 1);
  }
`;

Jeżeli to Twój pierwszy kontakt z styled components to pewnie zadajesz sobie jedno zasadnicze pytanie: cóż to za cholerstwo?

Jak sama nazwa biblioteki wskazuje: stylowany komponent.

Z styled components tworzymy komponenty opakowujące elementy DOM (np. styled.button, styled.div) lub inne komponenty (np. styled(Coin)).

Mamy możliwość warunkowego stylowania z wykorzystaniem props za pomocą wyjątkowo czytelnej składni.

// CoinListCell.js
const cellPadding = 10;
const largeCellFlex = '1 0 25%';
const smallCellFlex = '1 0 10%';
			
const Wrapper = styled.div`
  display: flex;
  align-items: center;
  flex: ${props => (props.isLarge ? largeCellFlex : smallCellFlex)};
  font-weight: ${props => (props.isHeader ? 700 : 300)};
  padding-top: ${cellPadding}px;
  padding-bottom: ${cellPadding}px;
  border-bottom: ${props => (props.isHeader ? 4 : 2)}px solid #dedede;
`;			

Style komponentu każdej instancji CoinListCell będą zależały od przypisania boolowskich propsów isLarge i isHeader.

// CoinListHead.js

const CoinListHead = () => (
  <CoinListRow>
    <CoinListCell isLarge isHeader>
      Name
    </CoinListCell>
    <CoinListCell isHeader>
      Price
    </CoinListCell>
    <CoinListCell isHeader>
      Change (24h)
    </CoinListCell>
    <CoinListCell isHeader>
      Market cap
    </CoinListCell>
    <CoinListCell isHeader>
      Circulating supply
    </CoinListCell>
  </CoinListRow>
);

Wszystkie props przekazane do styled componentu są przekazywane w dół. Świetnie widać to na przykładzie Input w komponencie SearchBar. Zobaczmy jak nowy komponent radzi sobie z obsługą atrybutów type, placeholder i value.

// SearchBar.js
const Input = styled.input`
  transition: all 0.3s ease-in-out;
  outline: none;
  padding: 0.5em 0.6em;
  margin: 5px 1px 3px 0;
  border: 1px solid #dddddd;
  border-radius: 4px;
  &:focus {
    box-shadow: 0 0 5px rgba(81, 203, 238, 1);
    border: 1px solid rgba(81, 203, 238, 1);
  }
`;

const SearchBar = ({ handleChange, searchQuery }) => {
  return (
    <Wrapper>
      <Input value={searchQuery} placeholder="Search" type="text" onChange={handleChange} />
    </Wrapper>
  );
};
	
// Przeglądarka 
<input class="sc-bxivhb hrnFMV" placeholder="Search" type="text" value="BTC">
	

Styled components, podobnie jak CSS Modules, dają nam możliwość kompozycji. W tak małej aplikacji jak crypto-tracker nie znalazłem zastosowania dla tej funkcjonalności, ale zobaczmy jak to wygląda na przykładzie z świetnie napisanej dokumentacji biblioteki.

	// The Button from the last section without the interpolations
const Button = styled.button`
  color: palevioletred;
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

// We're extending Button with some extra styles
const TomatoButton = Button.extend`
  color: tomato;
  border-color: tomato;
`;

render(
  <div>
    <Button>Normal Button</Button>
    <TomatoButton>Tomato Button</TomatoButton>
  </div>
);

Last but not least, styled-components ma bardzo podobne API dla React Web i React Native. Uczymy się raz, wykorzystujemy wszędzie.

Pokochałem styled components praktycznie od pierwszego wejrzenia. Podoba mi się cała filozofia łączaca to co najlepsze w CSS z tym co najlepsze w Reactie.

Tracker kryptowalut wystylizowany z użyciem styled components znajdziesz tutaj.

Cały ten mój zachwyt sprawił, że to właśnie branch styled-components został zmergowany z masterem i zostanie z trackerem do końca serii.

Panel crypto-trackera po dodaniu stylowania

Podsumowanie

Woah, może tego nie widać, ale ten wpis kosztował mnie dużo czasu i wysiłku. Zapoznanie się z stylowaniem w React i przygotowanie czterech różnych odsłon trackera zjadło kilkanaście godzin. W połączeniu z nagłym pojawieniem się wiosny zrobiły mi się trzy tygodnie przerwy od ostatniego wpisu. Zaniepokojonych zapewniam, że poziom mojej motywacji i chęci do prowadzenia bloga jest na równie wysokim poziomie jak w momencie jego założenia. W przyszłym tygodniu wracam do stałego rytmu jednego wpisu na tydzień ;). Do ukończenia aplikacji została jedynie integracja z REST API. Przed zabawą z AJAXem w React, musimy opanować metody cyklu życia komponentu. Przewijają się od samych początków tworzenia projektu, w kolejnym wpisie wyjaśnię do czego służą.

Kod źródłowy projektu jest dostępny tutaj.

Podobał Ci się dzisiejszy artykuł? Udostępnij go w Twoich ulubionych mediach społecznościowych. Może znajdzie się ktoś, dla kogo również będzie wartościowy.

Bądź na bieżąco z serią Pierwszy projekt w ReactJS. Wystarczy polubić fanpage AlgoSmart na fejsie, obserwować mój profil na Twitterze i regularnie odwiedzać portal Polski Front-end.

Do zobaczenia za tydzień :).

Zdjęcie tytułowe autorstwa: unsplash-logoCynthia del Río

Marcin Czarkowski

Cześć! Ja nazywam się Marcin Czarkowski, a to jest AlgoSmart - blog, na którym dzielę się wiedzą o React.js, JavaScript oraz CSS.

  • Łukasz

    Świetny przegląd, cieszę się, że odważnie i jednoznacznie polecasz konkretne podejście.

    Nie jest to kolejny wpis zakończony bełkotem typu: „Każdy musi sobie sam odpowiedzieć na pytanie, co mu najbardziej odpowiada”. Chwała za to.

    • Dzięki, też nie jestem zwolennikiem tego frazesu. Szkoda, że co raz częściej można się z nim spotkać. Odnoszę wrażenie, że wielu blogerów nie przyznaje się do własnych preferencji z względu na obawę przed urażeniem czytelnika. Pytanie czy warto przedkładać komfort nad możliwość podzielenia się swoim zdaniem i okazję do wymiany poglądów z kimś, kto się z nami nie zgadza (o zgrozo!).