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ż:
- Niekoniecznie wiemy gdzie zdefiniowane są klasy dla przycisku
- Jeżeli wiemy, niekoniecznie wiemy czy przypadkowo gdzieś ich nie nadpiszemy
- 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
- CSS in JS
- Styled-components
- Inne biblioteki CSS-in-JS
- Porównanie popularności bibliotek CSS-in-JS
- Podsumowanie
CSS in JS
CSS in JS to podejście czerpiące to co najlepsze z obu światów:
- Zapewnia pseudoselektory, media query i inne rzeczy ze zwykłego stylowania
- 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
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:
- Nie ma potrzeby importować funkcji
css
- Używamy składni camelCase zamiast snake-case, np
backgroundColor
zamiastbackground-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:
- Możliwość użycia skórek oraz dynamicznej podmiany skórki
- Ładowania CSS tylko tego CSS, który jest potrzebny dla wyrenderowanych komponentów
- 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:
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
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.