# [React] styled-component 筆記
###### tags: `React` `前端筆記` `style`
## 基本建立 `styled-component`
`styled-component` 是 `JS-in-CSS` 的解決方案,開發者可以透過 `JavaScript` 決定元素的樣式,與 `CSS module` 的方式不太一樣。
> 特別的是 `styled-component` 可以透過「函式」讀取外部傳入的 `props`,達到 `CSS module` 透過換 className 變更樣式。(`CSS module`:`state` 換 className,`styled-component`:`state` 換 `props`)
### 怎麼建立 `styled-component`?
```javascript!
import styled from 'styled-component'
const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
`
```
以上得到的結果與使用 `CSS module` 的結果相同
```javascript!
// xxx.jsx
// ...
<div className={style.container}></div>
// ...
// xxx.css
.container {
display: flex;
align-items: center;
justify-content: center;
}
```
## `props` 接收外部的資料
1. `props` 可以讀取 `<ThemeProvider theme={theme}>` 的 `theme`
2. `props` 可以讀取元件的 props
3. `props` 可以讀取該元素的屬性
```javascript!
const Btn = styled.button`
/* 讀取 button 的 disabled 屬性 */
color: ${(props) => (props.disabled ? "red" : "green")};
`
```
## 如何建立全局 style 變數?
一般使用 `CSS module` 時會建立 style 變數,存放重複性的設定(比方來說顏色、字體及 RWD 斷點等設定),在 `styled-component` 中則是提供 `<ThemeProvider>` 元件,讓其子層都可以讀取元件的樣式變數,達到重複使用 style 變數的效果。
### STEP1: 建立 `theme` 設定表
```typescript!
// 使用 TypeScript 的話就要先定義表的型別
export interface DefaultTheme
colors: {
primary: string;
secondary: string;
highlightText: string;
normalText: string;
lightenText: string;
lightenGrayishBlue: string;
white: string;
lightBoxBackground: string;
navBorderBottom: string;
black: string;
controllerBackground: string;
};
fontFamily: string;
}
export const theme: DefaultTheme = {
colors: {
primary: "#ff7d1a",
secondary: "#ffede0",
highlightText: "#1d2025",
normalText: "#68707d",
lightenText: "#b6bcc8",
lightenGrayishBlue: "#f7f8fd",
black: "#000",
white: "#ffffff",
lightBoxBackground: "rgba(0, 0, 0, .5)",
navBorderBottom: "#dee2e6",
controllerBackground: "#f1f1f1",
},
fontFamily: "Kumbh Sans",
};
```
### STEP2: 引入 `<ThemeProvider>`
透過 `<ThemeProvider>` 讓專案可以吃到設定的 style 變數:
> `<ThemeProvider>` 使用 `Context`,所以其子層都可以讀取 `<ThemeProvider>` 的 `theme`
```javascript!
// ...
import { theme } from '...'
<ThemeProvider theme={theme}>
// children components...
</ThemeProvider>
```
## 怎麼設定 style 初始化?
預設的元素會有一些很醜的設置,因此設定吃初始化(也就是建立全域 CSS)可以覆蓋預設的樣式,一般使用 `CSS module` 的話就是設定專案街口的 style 檔案,在 `styled-component` 則提供 `createGlobalStyle`(建立初始化 style)及 `<GlobalStyles />`(在專案引入初始化 style 的元件),供開發者設定初始化 style:
### STEP1: 建立 style 初始化的資料表
```javascript!
// global.ts
// ref. https://www.codevertiser.com/styled-components-folder-structure/
// 建立初始化 style 的方法
import { createGlobalStyle } from "styled-components";
import { DefaultTheme } from "./theme";
// 使用 TypeScript 時引入 style 變數的型別,讓初始化設定可以使用 style 變數
export default createGlobalStyle<{ theme: DefaultTheme }>`
body {
font-family: ${(props) => props.theme.fontFamily}
}
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-size: 100%;
}
body {
margin: 0;
padding: 0;
overflow-x: hidden;
min-height: 100vh;
text-rendering: optimizeSpeed;
font-family: ${({ theme }) => theme.fontFamily}, sans-serif;
font-size: 1rem;
line-height: 1;
}
h1,
h2,
h3,
h4,
h5,
h6,
p,
ul,
figure,
blockquote,
dl,
dd {
padding: 0;
margin: 0;
}
button {
border: none;
background-color: transparent;
font-family: inherit;
padding: 0;
cursor: pointer;
}
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
ul[role="list"],
ol[role="list"] {
list-style: none;
}
li {
list-style-type: none;
}
/* Set core root defaults */
html:focus-within {
scroll-behavior: smooth;
}
/* A elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
}
/* Make images easier to work with */
img,
picture {
max-width: 100%;
display: block;
}
/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
font: inherit;
}
/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
html:focus-within {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
`;
```
### STEP2: 透過 `<GlobalStyles />` 於專案引入初始化設定
```javascript!
// ...
<ThemeProvider theme={theme}>
// 1. 通常就直接放在 <ThemeProvider> 的第一層,讓專案可以吃到初始化設定
// 2. 因為放在 <ThemeProvider> 之內,所以可以透過函式讀取 theme 的變數
<GlobalStyles />
// children components...
</ThemeProvider>
```
### 官方文件範例
```javascript!
// Define our button, but with the use of props.theme this time
// 建立 button,並且從外部收顏色設定
const Button = styled.button`
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border-radius: 3px;
/* Color the border and text with theme.main */
color: ${props => props.theme.main};
border: 2px solid ${props => props.theme.main};
`;
// We are passing a default theme for Buttons that arent wrapped in the ThemeProvider
// 建立預設的 theme
Button.defaultProps = {
theme: {
main: "palevioletred"
}
}
// Define what props.theme will look like
// 建立 ThemeProvider 的 theme 資料表
const theme = {
main: "mediumseagreen"
};
render(
<div>
// 這裡會吃到預設的 theme
<Button>Normal</Button>
<ThemeProvider theme={theme}>
// 這裡會吃到 ThemeProvider 的 theme 資料表
<Button>Themed</Button>
</ThemeProvider>
</div>
);
```
## 怎麼建立 RWD 斷點設定?
也可以額外建立斷點設定的資料表,統一管理專案的斷點規範:
```javascript!
// ref. https://www.codevertiser.com/styled-components-folder-structure/
const size = {
xs: '400px', // for small screen mobile
sm: '600px', // for mobile screen
md: '900px', // for tablets
lg: '1280px', // for laptops
xl: '1440px', // for desktop / monitors
xxl: '1920px', // for big screens
}
// 最後會取得 '(max-width: xxx px)'
export const device = {
xs: `(max-width: ${size.xs})`,
sm: `(max-width: ${size.sm})`,
md: `(max-width: ${size.md})`,
lg: `(max-width: ${size.lg})`,
xl: `(max-width: ${size.xl})`,
xxl: `(max-width: ${size.xxl})`,
}
```
之後在元件內引入即可:
```javascript!
// ref. https://www.codevertiser.com/styled-components-folder-structure/
import styled, { ThemeProvider } from 'styled-components'
import { device } from './styles/BreakPoints'
import { theme } from './styles/Theme'
const Title = styled.h1`
color: ${(props) => props.theme.colors.primaryTextColor};
font-size: 48px;
@media ${device.md} {
font-size: 32px;
}
`
function App() {
return (
<ThemeProvider theme={theme}>
<Title>Hello world</Title>
</ThemeProvider>
)
}
export default App
```
## 延展 style
### 1. 繼承元件設定 `styled(元件)`
```javascript!
// 建立基本的 btn
const BaseBtn = styled.button`
color: red;
border: 1px solid black;
`
// 透過 `styled(元件)` 可以直接繼承該元件的 style 並重新包裝新的設定
const FancyBtn = styled(BaseBtn)`
background-color: green;
```
### 2. 直接拿元件,可以於內層元件內撰寫外層階級設定
打破一般 CSS 需要在外層定義內層的語法,直接在內層抓外層元素變更樣式:
```javascript!
// ref. https://styled-components.com/docs/advanced#referring-to-other-components
// 定義外層 a
const Link = styled.a`
display: flex;
align-items: center;
padding: 5px 10px;
background: papayawhip;
color: palevioletred;
`;
// 定義 svg
const Icon = styled.svg`
flex: none;
transition: fill 0.25s;
width: 48px;
height: 48px;
/* 強大的設定,可以直接於元件 style 設定中抓其他元件,並且不像一般 CSS 需要從外層 -> 內層定義,可以直接在內層定義外層設定 */
/* 等價於
a {
&:hover {
fill: rebeccapurple
}
}
*/
${Link}:hover & {
fill: rebeccapurple;
}
`;
const Label = styled.span`
display: flex;
align-items: center;
line-height: 1.2;
&::before {
content: '◀';
margin: 0 10px;
}
`;
// 渲染的階級是 a > svg | span
render(
<Link href="#">
<Icon viewBox="0 0 20 20">
<path d="M10 15h8c1 0 2-1 2-2V3c0-1-1-2-2-2H2C1 1 0 2 0 3v10c0 1 1 2 2 2h4v4l4-4zM5 7h2v2H5V7zm4 0h2v2H9V7zm4 0h2v2h-2V7z"/>
</Icon>
<Label>Hovering my parent changes my style!</Label>
</Link>
);
```

(可以發現 `a:hover` 時 svg 會改變顏色)
### 3. 也可以像是 `SCSS` 使用 `@extend` 定義重複性的屬性設置
使用 `SCSS` 時可以定義一個 class 處理重複性的屬性:
```sass!
.test {
display: flex;
align-items: center;
justify-content: center;
}
.target {
// .test 的屬性會被灌入 .target 的設定中
@extend test;
color: red;
}
```
`styled-component` 也提供 `css` 的方法定義重複性的樣式設定:
```typescript!
import styled, { css } from "styled-components";
// 這個方法可以建立原子性的 style -> 建立成一個常數,之後直接在不同的 styled-component 內使用
// 也可以透過定義型別,為 styled-component 增加彈性
const TestCss = css<{ backgroundColor?: string }>`
padding: 24px;
width: 40vw;
background-color: ${({ backgroundColor }) =>
backgroundColor as string | "auto"};
`;
export const Container = styled.div`
background-color: ${({ theme }) => theme.colors.white};
// 引用的方式
${TestCss}
// padding: 24px;
// width: 40vw;
`;
```
## 該如何透過 `props` 設定選擇性的屬性?
`styled-component` 會自動忽略 `falsy value`,所以可以透過這個特定建立選擇性的屬性:
```javascript!
// ref. https://styled-components.com/docs/advanced#tagged-template-literals
const Title = styled.h1`
/* Text centering won't break if props.upsidedown is falsy */
/* proos.upsidedown 不是 falsy value 時才會有 transform: rotate(180deg); 的屬性 */
${props => props.upsidedown && 'transform: rotate(180deg);'}
text-align: center;
`;
```
## Animation
`styled-component` 也有 `keyframse` 的方法建立動畫:
```javascript!
import styled, { keyframes } from "styled-components";
export const fadeIn = keyframes`
0% {
opacity: 0;
bottom: 0;
}
100% {
opacity: 1;
bottom: 5%;
}
`;
export const Container = styled.div`
animation: ${fadeIn} 0.5s linear;
`;
```
## 基本的專案結構設置
```shell!
src
├── assets --> 存放圖片、字體
| ├── fonts
| ├── icons
| └── images
├── components --> 元件
└── styles --> style 設定
├── breakpoints.ts -> 設定斷點
├── font.module.css -> 文字檔案
├── global.ts -> 初始化 CSS(全域 CSS)
└── theme.ts -> style 變數
```
## Recap
1. `styled-component` 可以透過 `props` 讀取 **元素屬性**、**元件 `props`**及**`<ThemeProvider theme>`**
2. 使用 TypeScript 時可以定義 styled component 的型別
3. 使用 `styled(component)`、直接在 styled component 抓其他 styled component 直接寫巢狀(與一般 `CSS` 需要從外到內定義的方式不同)及使用 `CSS` 定義重複的屬性
4. `createGlobalStyle` 定義全域(初始化)CSS 設定
## 參考資料
1. [React Styled Components Folder Structure Best Practices for Scalable Application](https://www.codevertiser.com/styled-components-folder-structure/)
2. [Documentation](https://styled-components.com/docs)
3. [[note] styled-component 筆記](https://pjchender.dev/npm/npm-styled-components/)