
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
backgroundColorzamiastbackground-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.
