Sprawdź 1 + 4 Biblioteki CSS-in-JS dla Reacta do użycia w 2020

Komponenty w React renderują UI na podstawie albo stanu wewnętrznego, albo przekazanych propsów:

const button = ({onClick, primary}) => {
  const [clicked, setClicked] = useState(false);

  const classes = classNames({
    'button': true,
    'button--active': clicked,
    'button--primary': primary,
  });

  return (
   <button
     onClick={() => {setClicked(!clicked); onClick();}}
     className={classes}
    >
     Click
    </button> 
   );
}

Choć widzimy i rozumiemy w jaki sposób dodawane są klasy, czy możemy być pewni jak komponent będzie wyglądał? Nie, ponieważ:

  1. Niekoniecznie wiemy gdzie zdefiniowane są klasy dla przycisku
  2. Jeżeli wiemy, niekoniecznie wiemy czy przypadkowo gdzieś ich nie nadpiszemy
  3. Jeżeli sprawdziliśmy i wiemy, że nie są nadpisane – ktoś kto używa tego komponentu poza naszym projektem może te klasy nadpisać i zmienić zachowanie komponentu nawet nie mając takiego zamiaru

Dodatkowo – jeżeli chcemy sprawdzić co konkretnie kryje się pod każdą klasą, musimy szukać w pliku stylów – poza komponentem. Musimy też o pliku stylów pamiętać podczas przenoszenia np do innego projektu, co ogranicza przenaszalność kodu.

Jeżeli chcielibyśmy uniknąć tego typu problemów możemy napisać style inline:

 const button = ({onClick, primary}) => {
  const [clicked, setClicked] = useState(false);

  return (
   <button
     onClick={() => {setClicked(!clicked); onClick();}}
     style={{
       //jakieś style bazowe
       backgroundColor: primary? 'blue': 'white',
       borderWidth: clicked? '2px' : '1px,
     }}
    >
     Click
    </button> 
   );
} 

Ale zabiera nam to na przykład pseudoselektory czy media query.

Jednym z możliwych rozwiązań jest CSS-in-JS.


Spis treści

  1. CSS in JS
  2. Styled-components
  3. Inne biblioteki CSS-in-JS
    1. Emotion
    2. JSS
    3. Styled-jsx
    4. Aphrodite
  4. Porównanie popularności bibliotek CSS-in-JS
  5. Podsumowanie

CSS in JS

CSS in JS to podejście czerpiące to co najlepsze z obu światów:

  1. Zapewnia pseudoselektory, media query i inne rzeczy ze zwykłego stylowania
  2. Pozwala nam zapomnieć o problemach z klasami CSS

CSS in JS to podejście a nie biblioteka – może zostać zaimplementowane w różny sposób. Główna idea jest taka, że piszemy style niejako “inline” w komponencie, biblioteka transformuje je do selektorów CSS – na przykład do klas.
Dzięki temu że kod HTML (JSX), JS i CSS są zawarte w jednym pliku komponenty są łatwo przenaszalne. Ponieważ style są wpisane w komponent widzimy bezpośrednio jak zmiany w stanie/propsach wpłyną na komponent – nie musimy nigdzie szukać jaki kolor przycisku ustawia np klasa button--primary.
Jeśli komuś pojęcie CSS in JS jest znajome, pierwszym skojarzeniem jest zapewne biblioteka styled-components.


Styled-components

Styled components zostało przeze mnie opisane w poprzednim wpisie:

Styled-components nie jest jednak jedyną opcją.


Inne biblioteki CSS-in-JS

  1. Emotion
  2. JSS
  3. Styled-jsx
  4. Aphrodite

Emotion

Emotion to biblioteka CSS-in-JS niepowiązana bezpośrednio z Reactem. Wystarczy zainstalować główną paczkę:

npm i emotion

I można używać Emotion bez konieczności grzebania w konfiguracji – więc bez problemu użyjemy jej w aplikacji CRA. W porównaniu do styled-components ma nieco inną składnię – w standardowej wersji styl przypisujemy do parametru className:

const padding = 10px;
const margin = 5px;

const styles = css`
  padding: ${padding};
  margin: ${margin};
  border-radius: 5px;
`

render(
  <div
    className={styles}
  >
    Some content.
  </div>
);

Jeżeli chcemy dopisać np pseudoselektor, używamy składni wklejania selektoru rodzica:

const styles = css`
  padding: ${padding};
  margin: ${margin};
  border-radius: 5px;

  &:hover {
    border-radius: 0;
  }
`

Składnia ta jest bardzo często używana w SCSS do zagnieżdżania selektorów bądź do uproszczonego zapisu klas w metodologii BEM.

Emotion-core dla Reacta

By móc lepiej wykorzystać Emotion w React, możemy zainstalować bibliotekę emotion-core dedykowaną dla Reacta:

npm i @emotion/core

Niestety, paczka ta nie zadziała z automatu z aplikacją CRA i trzeba będzie zrobić eject by móc zmodyfikować konfigurację – zyskamy jednak wsparcie dla Server-Side Renderingu.

Składnia nieco różni się od wersji głównej – zamiast do className przypisujemy do atrybutu css:

const padding = 10px;
const margin = 5px;

const styles = css`
  padding: ${padding};
  margin: ${margin};
  border-radius: 5px;
`

return (
  <div
    css={styles}
  >
    Some content.
  </div>
);

Do parametru css możemy przekazywać tablicę, co pozwala nam łatwo komponować style i zachować porządek w stylowaniu:

const padding = 10px;
const margin = 5px;

const base = css`
  padding: ${padding};
  margin: ${margin};
  border-radius: 5px;
  background: green;
`;

const normal = css`
  background: white;
  color: black;
`;

const dark = css`
  background: black;
  color: white;
`

return (
  <div
    css={[styles, darkMode? dark : white;]}
  >
    Some content.
  </div>
);

Style nadawane są w kolejności ich użycia, ostatni nadpisuje poprzednie – co w powyższym przykładzie oznacza, że początkowa deklaracja background: green zostanie nadpisana którąś z deklaracji występujących później.

Przekazywanie parametrów jako obiekt

Zamiast template string możemy do parametru css przekazać obiekt. Pod kilkoma względami może być to lepsze rozwiązanie niż template string:

  1. Nie ma potrzeby importować funkcji css
  2. Używamy składni camelCase zamiast snake-case, np backgroundColor zamiast background-color – dzięki temu zachowujemy spójność z czystym JS, w którym do stylu elementu odnosimy się poprzez camelCase: element.style.backgroundColor = 'white';

Przykład – pogrubione kody są równoważne:

const padding = 10px;
const margin = 5px;

const styles = css`
  padding: ${padding};
  margin: ${margin};
  border-radius: 5px;
`

const styles = {
  padding,
  margin,
  borderRadius: '5px'
}

return (
  <div
    css={styles}
  >
    Some content.
  </div>
);

Używając obiektów możemy również stosować np pseudoselektory zagnieżdżając kolejny obiekt – wtedy jednak należy nazwę klucza ująć w znaki cudzysłowu:

const styles = {
  padding,
  margin,
  borderRadius: '5px'
  '&:hover': {
    borderRadius: 0
  }
}

Emotion theme (skórki)

Podobnie jak styled-components, emotion umożliwia tworzenie skórek. Należy zainstalować paczkę emotion-theming:

npm install -S emotion-theming

Następnie trzeba użyć ThemeProvider do dostarczenia skórki do naszej aplikacji. Skórka może być jednym ze sposobów implementacji przełączania pomiędzy trybem jasnym i ciemnym:

const Button = (props) => {
  return (
    <button
      css={theme => {
        return {
          color: theme.textColor,
          backgroundColor: theme.backgroundColor,
        };
      }}
    >
     {props.children}
    </button>
  )
}

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>Save</Button
</ThemeProvider> 

Theming jest dostępny tylko jeżeli użyjemy wersji Reactowej, @emotion/core

Emotion-styled

Choć osobiście składnię Emotion uważam za czytelniejszą niż składnię styled-components, dla uważających inaczej jest możliwość zainstalowania paczki emotion-styled i pisania kodu w API podobnym do styled-components:

npm i @emotion/styled

Przykładowy kod mógłby wyglądać tak:

const padding = 10px;
const margin = 5px;

const StyledDiv = styled.div`
  padding: ${padding};
  margin: ${margin};
  border-radius: 5px;
`

return (
   <StyledDiv>
    Some content.
  </StyledDiv>
);

Ciekawostka o Emotion

Emotion została oficjalnym następcą biblioteki Glamorous – zaimplementowany został nawet skrypt automatyzujący migrację do Emotion.


JSS

Biblioteka JSS jest niezależna od Reacta, style deklarujemy w niej jako obiekt gdzie klucz będzie przypisany do klasy elementu HTML a wartość będzie stylami tej klasy:

jss.setup(preset())
const styles = {
  '@global': {
    body: {
      color: 'green'
    },
    a: {
      textDecoration: 'underline'
    }
  },
  button: {
    backgroundColor: 'black',
    color: 'white',
    fontSize: 12,
    '&:hover': {
      filter: 'brightness(110%)'
    }
  },
  buttonSuccess: {
    extend: 'button',
    backgroundColor: 'green'
  }
}

const { classes } = jss.createStyleSheet(styles).attach()

document.body.innerHTML = `
  <button class="${classes.button}">Regular</button>
  <button class="${classes.buttonSuccess}">Success</button>
` 

JSS posiada dedykowaną paczkę dla Reacta: React-JSS. Zajmiemy się nią, ponieważ podobnie jak w przypadku Emotion paczka ta dodaje sporo nowych możliwości do biblioteki głównej.

React-JSS

React-JSS dodaje kilka ciekawych funkcjonalności:

  1. Możliwość użycia skórek oraz dynamicznej podmiany skórki
  2. Ładowania CSS tylko tego CSS, który jest potrzebny dla wyrenderowanych komponentów
  3. Dodawania/usuwania stylów w momencie renderowania/usuwania komponentu

Podstawy React-JSS

Do utworzenia stylów używamy funkcji createUseStyles, która zwróci nam hooka którego używać będziemy w komponencie. createUseStyles to tzw funkcja wyższego rzędu. Jako parametr bierze obiekt z nazwami klas jako klucze i obiektami jako stylami. Zamiast obiektu, jako styl możemy przekazać funkcję która jako parametr przyjmuje propsy komponentu:

import {createUseStyles} from 'react-jss';

const useStyles =  createUseStyles({
  button: {
    color: 'white',
    backgroundColor: 'black',
  },
  text: props => {
    return {
      padding: 10,
      color: props.color,
    }
  }
});


const Button = (props) => {
  const classes = useStyles(props)
  return ( 
    <button className={classes.button}>
      <span className={classes.text}>{props.children}</span>
    </button>
  );
};

<Button>Text</Button>

Skórki w React-JSS

Jak każda szanująca się biblioteka CSS-in-JSS React-JSS daje nam możliwość używania skórek. Wewnątrz komponentów zawierających się w ThemeProvider theme dostępny jest w propsach:

import {createUseStyles, useTheme, ThemeProvider} from 'react-jss';

const theme = {
  borderRadius: 5,
};

const useStyles =  createUseStyles({
  button: props => {
    return  {
      color: 'white',
      backgroundColor: 'black',
      borderRadius: props.theme.borderRadius
    } 
  },
  text: props => {
    return {
      padding: 10,
      color: props.color,
    }
  }
});

const Button = (props) => {
  const classes = useStyles(props)
  return ( 
    <button className={classes.button}>
      <span className={classes.text}>{children}</span>
    </button>
  );
};

<ThemeProvider theme={theme}><Button>Text</Button></ThemeProvider>

Styled-jsx

Używanie biblioteki styled-jsx znacząco różni się od poprzednich – pisząc w styled-jsx mamy podobne wrażenia jak podczas pisania zwykłego CSS:

const Button = props => {
  return (
    <>
      <p>First element<p>
      <span className={'text'}>Second element</span>
       <style jsx>{
         p {
           color: red;
         }
         .text {
           color: blue;
         }
       `}</style> 
    </>
  );
}

Ten sposób zapisu będzie zaletą dla tych, którzy przywykli do klasycznego sposobu pisania stylów w CSS. Dodatkowo, taki zapis bardzo ułatwia pisanie innych selektorów niż klasy, a takie też są nadal potrzebne.

Style dynamiczne

Ponieważ stylowanie w styled-jsx używa template string, możemy używać dynamicznych wartości z propsów/stanu:

const Button = props => {
  return (
    <>
      <p>First element<p>
      <span className={'text'}>Second element</span>
       <style jsx>{
         p {
           color: red;
           padding: ${props.padding || '5px'}
         }
         .text {
           color: blue;
         }
       `}</style> 
    </>
  );
}

Skórki

Styled-jsx nie oferuje bezpośrednio wsparcia dla skórek i przełączania między nimi, można jednak zasymulować to zachowanie poprzez przekazywanie theme jako propsa:

const Button = props => {
  return (
    <>
      <p>First element<p>
      <span className={'text'}>Second element</span>
       <style jsx>{
         p {
           color: red;
           padding: ${props.theme.padding}
         }
         .text {
           color: blue;
         }
       `}</style> 
    </>
  );
}

Aphrodite

Aphrodite jest biblioteką utworzoną przez twórców Khan Academy, pierwotnie była używana wewnętrznie a potem została upubliczniona. Jej rozmiar to zaledwie 20 kilobajtów przed / 6 kilobajtów po skompresowaniu przez gzip.

Po zainstalowaniu poleceniem npm install --save aphrodite style tworzymy jako obiekty przekazywane do Stylesheet.create(...), gdzie klucze to nazwy klas a wartości to wartości stylów:

import React from 'react';
import { StyleSheet, css } from 'aphrodite'; 

const styles = Stylesheet.create({
  button: {
    color: 'white',
    backgroundColor: 'black',
    ':hover': {
      backgroundColor: 'gray'
    }
  }
});

const Button = props => {
  return (
    <button className={css(styles.button)}>{props.children}</button>
  );
};

<Button>Text</Button>

Aphrodite dostacza możliwość rozszerzania funkcjonalności biblioteki poprzez dopisywanie rozszerzeń – nie wymaga to zmian w konfiguracji projektu itp.

Porównanie popularności bibliotek CSS-in-JS

Do porównania popularności bibliotek użyjemy strony npm-stats, udostępniającą dane dotyczące pobrań biblioteki w danym dniu. Jako dzień sprawdzenia statystyk przyjmiemy 21 lutego 2020 roku.

Porównanie dla paczek styled-components, emotion, jss, styled-jsx, aphrodite

Najpierw porównanie dla paczek bazowych, pamiętając o tym że Emotion dostarcza również paczkę @emotion/core a JSS paczkę React-JSS:

https://npm-stat.com/charts.html?package=styled-components&package=emotion&package=jss&package=styled-jsx&package=aphrodite&from=2020-02-22&to=2020-02-22:

Porównanie: styled-components, emotion, jss, styled-jsx, aphrodite

Wyraźnie widać że styled-components (prawie 57 tysięcy pobrać) i JSS (prawie 49,5 tysiąca pobrań) mają dość wyraźną przewagę nad resztą stawki – kolejne Emotion ma dwa razy słabszy wynik – niewiele ponad 24 tysiące pobrań.

Porównanie dla paczek styled-components, @emotion/core, react-jss, styled-jsx, aphrodite

https://npm-stat.com/charts.html?package=styled-components&package=%40emotion%2Fcore&package=react-jss&package=styled-jsx&package=aphrodite&from=2020-02-22&to=2020-02-22:

Porównanie: styled-components, @emotion/core, react-jss, styled-jsx, aphrodite

Przy takim układzie wynik Emotion wygląda nieporównywalnie lepiej.

Podsumowanie

Przez spory okres czasu, gdy nie interesowałem się tematem CSS-in-JS, myślałem że styled-components to jedyna opcja. Okazało się, że bibliotek jest wiele – tak jak wiele jest podejść. Wg mnie najlepiej wygląda Emotion – najczytelniejsza składnia i duża popularność – ale zdania mogą być różne. Pod adresem https://www.cssinjsplayground.com/ można sprawdzić jeszcze dodatkowe biblioteki CSS-in-JS.