有種用了就回不去的感覺。
styled component 中的 props
既然它是 Component,那當然也有 props 可以用,直接來示範怎麼用:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 
 | const Button = styled.button`border-radius: 4px;
 padding: 8px 12px;
 background-color: #e4e4e4;
 border: none;
 color: black;
 cursor: pointer;
 flex-shrink: 0;
 
 & + & {
 margin-left: 8px;
 }
 
 ${
 props =>  props.isDone && `
 background-color: #0e920e66;
 color: white;
 `
 }
 `
 
 function TodoItem ({ content, handlebuttonClick }) {
 return (
 <TodoItemWrapper>
 <TodoContent>{content}</TodoContent>
 <TodoButtonWrapper>
 // 在這裡傳入
 <Button isDone={true}>已完成</Button>
 <Button onClick={handlebuttonClick}>刪除</Button>
 </TodoButtonWrapper>
 </TodoItemWrapper>
 )
 }
 
 | 

在 styled Component 接收 props 的方式是透過 ${...},${...} 裡面會寫一個 function 來接收 props 參數,你就可以根據它來寫不同的  style 了。
繼承 style
這個跟 class 中的繼承有點類似,你可以先繼承某個 styled component, 再下新的 style 來覆寫。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | const Button = styled.button`border-radius: 4px;
 padding: 8px 12px;
 background-color: #e4e4e4;
 border: none;
 color: black;
 cursor: pointer;
 flex-shrink: 0;
 
 & + & {
 margin-left: 8px;
 }
 
 ${
 props =>
 props.size === 'XL' ? 'font-size:20px' : 'font-size: 12px'
 }
 `
 
 
 const PinkButton = styled(Button)`
 background-color: pink;
 `
 
 | 

如果要對「React」的 component 重新設定 style
先講一個觀念:
只有 styled component 才吃的到樣式,所以你用的時候一定是放 styled component,不是 react component。
用 React component 包裝起來的 styled components
這種要搭配 props 來傳入 className:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | function TodoItem ({ className, children, handlebuttonClick }) {return (
 <TodoItemWrapper className={className}>
 <TodoContent>{children}</TodoContent>
 <TodoButtonWrapper>
 <Button>已完成</Button>
 <Button onClick={handlebuttonClick}>刪除</Button>
 </TodoButtonWrapper>
 </TodoItemWrapper>
 )
 }
 
 
 
 const TodoItem2 = styled(TodoItem)`
 background-color: #d6d6d6;
 `
 
 | 

- 用 style component 來包裝的 React component
跟剛剛反過來:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | const OrangeButton = styled(TestButton)`
 ${defaultButton}
 border-color: ${({ theme }) => theme.orange};
 color: ${({ theme }) => theme.orange};
 &:hover {
 background-color: ${({ theme }) => theme.orange};
 }
 `;
 
 
 function TestButton ({ className }) {
 return <button className={className}>測試用</button>
 }
 
 | 

Medai query
其實就是直接在 styled 中寫 @media 就行了,不過更好的做法是把 breakpoint 拆出來寫成常數引入會更好:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | import { MEDIA_TABLET, MEDIA_PC } from './constants/breakpoint'
 
 const Button = styled.button`
 border-radius: 4px;
 padding: 8px 12px;
 background-color: #e4e4e4;
 border: none;
 color: black;
 cursor: pointer;
 flex-shrink: 0;
 
 & + & {
 margin-left: 8px;
 }
 
 ${MEDIA_TABLET} {
 font-size: 1.2em;
 }
 
 ${MEDIA_PC} {
 font-size: 1.5em;
 }
 
 `
 
 | 

變數的運用
首先要用 <ThemeProvider> 把整個 component 給包住:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | import { ThemeProvider } from 'styled-components';
 
 const theme = {
 blue: 'royalblue',
 orange: 'darkorange',
 green: 'mediumseagreen',
 red: 'palevioletred',
 }
 
 ReactDOM.render(
 
 <ThemeProvider theme={theme}>
 <App />
 </ThemeProvider>,
 document.getElementById('root')
 )
 
 | 
接著在其他的 style component 就可以透過 props.theme 來存取:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | const Button = styled.button`border-radius: 4px;
 padding: 8px 12px;
 background-color: transparent;
 border: 1px solid ${props => props.theme.blue};
 color: ${props => props.theme.blue};
 cursor: pointer;
 flex-shrink: 0;
 
 & + & {
 margin-left: 8px;
 }
 `
 
 const GreenButton = styled(Button)`
 border-color: ${props => props.theme.green};
 color: ${props => props.theme.green};
 `
 const OrangeButton = styled(Button)`
 border-color: ${props => props.theme.orange};
 color: ${props => props.theme.orange};
 `
 const RedButton = styled(Button)`
 border-color: ${props => props.theme.red};
 color: ${props => props.theme.red};
 `
 
 | 
成果大概就像這樣:

不同的標籤想套用相同樣式
假設我寫了一個 <button> 的 styled component,但今天又想在 <a> 上用一樣的樣式時,不需要重新寫一遍,只要利用 as 就可以了:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 
 | const Button = styled.button`border-radius: 4px;
 padding: 8px 12px;
 background-color: transparent;
 border: 1px solid ${props => props.theme.blue};
 color: ${props => props.theme.blue};
 cursor: pointer;
 flex-shrink: 0;
 
 & + & {
 margin-left: 8px;
 }
 &:hover {
 background-color: ${props => props.theme.blue};
 color: white;
 }
 `
 
 function TestScope () {
 return (
 <TodoButtonWrapper style={{
 marginTop: '20px'
 }}>
 <Button>Button</Button>
 // 用 as 變成希望的標籤
 <Button as="a" href="#">Link</Button>
 <Button as="a" href="#">Link</Button>
 </TodoButtonWrapper>
 )
 }
 
 | 

或甚至是變成另一個 component 也行:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | function TestScope () {return (
 <TodoButtonWrapper style={{
 marginTop: '20px'
 }}>
 <Button>Button</Button>
 <Button as={ReversedButton}>Button</Button>
 <ReversedButton>顛倒文字的按鈕</ReversedButton>
 
 </TodoButtonWrapper>
 )
 }
 
 function ReversedButton ({ children }) {
 return <Button>{children.split('').reverse()}</Button>
 }
 
 | 

自動判斷是不是 HTML 元素的屬性
| 12
 3
 4
 5
 6
 7
 
 | <TodoItemWrapper><TodoContent>{todo.content}</TodoContent>
 <TodoButtonWrapper>
 <GreenButton isDone={todo.isDone} data-id={todo.id} onClick={handleCompletedButtonClick}>{todo.isDone ? '已完成' : '未完成'}</GreenButton>
 <RedButton data-id={todo.id} onClick={handleRemoveButtonClick}>刪除</RedButton>
 </TodoButtonWrapper>
 </TodoItemWrapper>
 
 | 
以這個例子來說,GreenButton 寫了 isDone 和 data-id 兩個屬性,但實際上會被渲染出來的只有 data-id,不會有 isDone:

這就是 style component 會做的自動判斷。
不過有一種情況是可能我想在 style component 傳 id 這個 props:
| 12
 3
 4
 
 | <GreenButton id={todo.id}
 onClick={handleButtonClick('isCompleted')}>{todo.isDone ? '已完成' : '未完成'}
 </GreenButton>
 
 | 
可是又不希望被渲染到 HTML 上,這時候就可以改用 $ 來傳(Transient props):
| 12
 3
 4
 
 | <GreenButton $id={todo.id}
 onClick={handleButtonClick('isCompleted')}>{todo.isDone ? '已完成' : '未完成'}
 </GreenButton>
 
 | 
這樣就不會被渲染了。
所以建議養成一種習慣,只要是給 style component 用的 props 就一律用 $ 來表示,可讀性會更好。
設定屬性值
要在 style component 上設定 HTML 元素的屬性有兩種方式,第一種是直接寫在 Component 上:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | const RadioButton = styled.input``
 function TestScope () {
 return (
 <TodoButtonWrapper style={{
 marginTop: '20px'
 }}>
 <RadioButton type="radio" name='gender' value="man"></RadioButton>
 <RadioButton type="radio" name='gender' value="female"></RadioButton>
 </TodoButtonWrapper>
 )
 }
 
 | 
第二種是透過 style.attrs 屬性:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | const RadioButton = styled.input.attrs({ type: 'radio' })``
 function TestScope () {
 return (
 <TodoButtonWrapper style={{
 marginTop: '20px'
 }}>
 <RadioButton name='gender' value="man"></RadioButton>
 <RadioButton name='gender' value="female"></RadioButton>
 </TodoButtonWrapper>
 )
 }
 
 | 
看起來是第二種會好一點,因為只要寫在一個地方就好,之後要改會比較方便。
設定全域空間的樣式
當想要改 <body> 或是 reset 的樣式時,應該就會用到。這是透過 createGlobalStyle 來達成的。
首先要先寫好全域的 style component:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | import { createGlobalStyle } from "styled-components";
 export const GlobalStyle = createGlobalStyle`
 body {
 // 全域空間的 style
 background-color: pink;
 }
 .bg-dark {
 background-color: black;
 }
 `
 
 | 
接著引入到 entry 就會套用了:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | ReactDOM.render(<ThemeProvider theme={theme}>
 // 放在這裡,不是外面
 // 另外因為是包在 ThemeProvide 裡,所以也能存到 theme 的 props
 <GlobalStyle />
 <App />
 </ThemeProvider>,
 document.getElementById('root')
 )
 
 | 
定義在 Global 的 className 也可以在底下的 component 使用:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | function TestScope () {return (
 <TodoButtonWrapper className="bg-dark">
 <Button>123</Button>
 <Button>456</Button>
 <Button>789</Button>
 </TodoButtonWrapper>
 )
 }
 
 | 

給選取器更高的權重:&&
先來看段 code:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | const Span = styled.span`
 && {
 color: blue
 }
 `
 
 const GlobalStyle = createGlobalStyle`
 body {
 // 全域空間的 style
 }
 span${Span} {
 color: red;
 }
 `
 
 ReactDOM.render(
 <ThemeProvider theme={theme}>
 <GlobalStyle />
 <App />
 <Span>I'm span</Span>
 </ThemeProvider>,
 document.getElementById('root')
 )
 
 | 
簡單來說,我在 Global 宣告 span${Span} 要是紅色,但我希望實際是藍色,所以就在 Span 中用 && 來覆寫全域設定:

注意 & 跟 && 的差別,一個會被覆寫一個不會。
把會重複使用的樣式存起來
style component 有提供 css 方法讓你把會共用的樣式儲存起來,它的寫法是這樣:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 
 | import { css } from 'styled-components'
 const buttonDefault = () => css`
 border-radius: 4px;
 padding: 8px 12px;
 background-color: transparent;
 cursor: pointer;
 flex-shrink: 0;
 
 // 這個寫法會有一些問題,留到下面來解釋
 & + & {
 margin-left: 8px;
 }
 `
 const BlueButton = styled.button`
 // 直接引用
 ${buttonDefault}
 border: 1px solid ${props => props.theme.blue};
 color: ${props => props.theme.blue};
 &:hover {
 background-color: ${props => props.theme.blue};
 color: white;
 }
 `
 const GreenButton = styled.button`
 // 直接引用
 ${buttonDefault}
 border-color: 'green';
 color: 'green';
 &:hover {
 background-color: 'green';
 }
 `
 const OrangeButton = styled.button`
 // 直接引用
 ${buttonDefault}
 border-color: 'orange';
 color: 'orange';
 &:hover {
 background-color: 'orange';
 }
 `
 const RedButton = styled.button`
 // 直接引用
 ${buttonDefault}
 border-color: 'red';
 color: 'red';
 &:hover {
 background-color: 'red';
 }
 `
 
 | 
這樣就能做出這樣的效果:

不過會發現 & + & 的部分並沒有套用到,為什麼?
這是因為當你建立一個新的 style component 時其實會重新產生一個 className,所以你的 & + & 其實是這樣子:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | BlueButton + BlueButton {margin-left: 8px
 }
 greenButton + greenButton {
 margin-left: 8px
 }
 RedButton + RedButton {
 margin-left: 8px
 }
 
 | 
上圖中的綠按鈕旁邊接的是紅按鈕,所以才沒有套用這個規則。
要解決這個問題的辦法有兩種,一種是改用「繼承」的寫法:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | const BlueButton = styled.button`border-radius: 4px;
 padding: 8px 12px;
 background-color: transparent;
 border: 1px solid ${props => props.theme.blue};
 color: ${props => props.theme.blue};
 cursor: pointer;
 flex-shrink: 0;
 
 & + & {
 margin-left: 8px;
 }
 &:hover {
 background-color: ${props => props.theme.blue};
 color: white;
 }
 `
 const GreenButton = styled(BlueButton)`
 // ...
 `
 const OrangeButton = styled(BlueButton)`
 // ...
 `
 const RedButton = styled(BlueButton)`
 // ...
 `
 
 | 
因為是透過繼承,所以 BlueButton 的部分不會重新產生 className,只有後來新增的才會。
另一種方式是把 & + & 的規則透過父層來指定:
| 12
 3
 4
 5
 6
 7
 8
 
 | const TodoButtonWrapper = styled.div`display: flex;
 align-items: center;
 // 底下的 button + button 會套用
 button + button {
 margin-left: 8px;
 }
 `
 
 | 
至於哪種寫法比較好就見仁見智,我個人是覺得繼承的寫法比較直覺一點,不過缺點是元件本身會多一條限制,因此「可重用性」會比較差。
其他補充
- style.button跟- style('button')是等價的東西
- 不要把 style component 寫在 React component 裡面,會有效能問題(每次 render 就重新宣告)