[note] styled-component 筆記
Transient Props:避免 props 傳入到子層元件
如果出現 React does not recognize the `fooBar` prop on a DOM element
的錯誤,表示 Styled Components 會把這個值傳到最終的 DOM 元素上,但 DOM 元素上並沒有 fooBar
這個屬性,所以會發生錯誤。
要避免這個問題只需要使用 transient props,也就是在變數前面加上 $
的做法即可:
const TextWithAbnormalStatus = styled(Typography)<{
$fooBar: AbnormalStatus;
}>`
color: ${({ $fooBar }) => {
return $fooBar === 'transient' ? 'red' : 'blue';
}};
`;
const Example = () => <TextWithAbnormalStatus $fooBar="transient" />;
- How to extend styled component without passing props to underlying DOM element? @ stackoverflow
- transient-props @ styled-components
Styling any Components
如果有第三方或自己寫的元件(components)想要變成 styled components 的話,只需在原本自己的元件上加上 className
,讓 styled-components 可以為它添加 class 名稱:
// This could be react-router-dom's Link for example
const Link = ({ className, children }) => <a className={className}>{children}</a>;
const StyledLink = styled(Link)`
color: palevioletred;
font-weight: bold;
`;
render(
<div>
<Link>Unstyled, boring Link</Link>
<br />
<StyledLink>Styled, exciting Link</StyledLink>
</div>,
);
Styling any components @ styled-components
Sample Code
Styling any components @ CodeSandbox
Props
如果你要 styled 的是:
- 是 一般的 HTML 元素(例如
styled.div
),styled-components 會把所有已知合法的 HTML Attributes 當成 props 傳入; - 是 React 元件(例如,
styled(MyComponent)
),那麼 styled-components 會把所有的 props 當成 props 傳入。
如果是一般的 HTML 元素,想要加入非正規的 HTML 屬性時,需要使用
.attrs()
方法。
attrs
透過 .attrs()
方法,除了自定 HTML 屬性放到 HTML 元素上,還可以使用動態的屬性(dynamic attributes):
const Input = styled.input.attrs({
// 這裡可以定義合法或自訂的 HTML attributes
type: 'password',
foo: 'bar',
// 也可以動態定義 HTML 屬性
margin: (props) => props.size || '1em',
padding: (props) => props.size || '1em',
})`
color: palevioletred;
font-size: 1em;
/* 在這裡可以直接拿動態的屬性來用 */
margin: ${(props) => props.margin};
padding: ${(props) => props.padding};
`;
render(
<div>
<Input placeholder="A small text input" size="1em" />
<br />
<Input placeholder="A bigger text input" size="2em" />
</div>,
);
Attaching Additional Props @ styled-components
attrs
搭配 TypeScript 的用法,可以參考 How do you use attrs with TypeScript?:
// https://github.com/styled-components/styled-components/issues/1959#issuecomment-781272613
const Container = styled.div.attrs<{ size: number }>((props) => {
return {
width: props.size,
height: props.size,
};
})<{ width: number; height: number }>`
// The outer type
width: ${(props) => props.width}px;
height: ${(props) => props.width}px;
`;
/**
* 也可以偷懶把 attrs 要用的 props 和 styled 用到的 props 寫在一起
**/
interface IContainer {
size: number;
width?: number;
height?: number;
}
const Container = styled.div.attrs<IContainer>((props) => {
return {
width: props.size,
height: props.size,
};
})<IContainer>`
// The outer type
width: ${(props) => props.width}px;
height: ${(props) => props.width}px;
`;
const container = <Container size={200} />;
Sample Code
Attaching Additional Props @ CodeSandbox
Refer to other Components
const Text = styled.span`
font-weight: bold;
`;
const Headline = styled.h3`
padding: 5px 10px;
background: papayawhip;
color: gray;
// Headline 裡的 Text 會套用 color
// 也就是 Headline Text { ... } 的意思
${Text} {
color: blue;
}
`;
const Link = styled.a`
display: inline-flex;
align-items: center;
color: palevioletred;
// 當 Headline 被 hover 的時候, "&" 會套用 color
// 也就是 Headline:hover Link {...} 的意思
${Headline}:hover & {
color: rebeccapurple;
}
`;
const Watchout = () => (
<Headline>
<Text>Refer</Text> to <Link>Watchout</Link>
</Headline>
);
Refer to other components @ CodeSandbox
Animations
在 CSS Animations 中 @keyframes
通常不會被縮限在一個元件中,但使用上卻又不希望它跑到全域造成命名衝突,因此在 styled-components 中有一個 keyframes
的 helper,讓我們可以使用同一個 @keyframes 但又不會有命名衝突:
import styled, { keyframes } from 'styled-components';
// 建立 keyframes
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
// 使用定義好的 rotate keyframes
const Rotate = styled.div`
display: inline-block;
animation: ${rotate} 2s linear infinite;
padding: 2rem 1rem;
font-size: 1.2rem;
`;
render(<Rotate>< 💅 ></Rotate>);
Sample Code
const fadeInOutAnimation = keyframes`
0% { opacity: 0; }
40% { opacity: 0; }
45% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; }
`;
const FadeInOut = styled.div`
animation-name: ${fadeInOutAnimation};
`;
Animation @ CodeSandbox
Theme
在 styled-components 中還提供 <ThemeProvider>
這個 wrapper 元件,這個元件透過 context API 可以把內層的所有 React 元件都提供 theme
props。
// Define our button, but with the use of props.theme this time
// STEP 3:在原本的 React Component 中,可以透過 props.theme 取得先前定義好的物件
const Button = styled.button`
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border-radius: 3px;
/* 根據 theme.main 顯示顏色 */
color: ${(props) => props.theme.main};
border: 2px solid ${(props) => props.theme.main};
`;
// 對於沒有綁定的可以使用預設值
Button.defaultProps = {
theme: {
main: 'palevioletred',
},
};
// STEP 1: 定義要帶入 theme props 的物件
const theme = {
main: 'mediumseagreen',
};
// STEP 2: 透過 <ThemeProvider> 把物件帶入 theme props 中
render(
<div>
<Button>Normal</Button>
<ThemeProvider theme={theme}>
<Button>Themed</Button>
</ThemeProvider>
</div>,
);
Sample Code
ThemeProvider @ CodeSandbox
Babel Plugin
Babel Plugin @ styled-components
TypeScript
TypeScript @ Styled Components
Sample Code
react-styled-component-sandbox @ CodeSandbox
Using Custom Props
Styled Component 在搭配 TypeScript 使用 props 時,只需要在 element 或 component 後定義帶入的型別即可:
// https://styled-components.com/docs/api#using-custom-props
enum AlertStatusEnum {
Unknown = 0,
Normal = 1,
Mild = 2,
Severe = 3,
}
interface StatusTextProps {
alertStatus: AlertStatusEnum;
}
const StatusText = styled.span<StatusTextProps>`
font-size: 20px;
line-height: 28px;
color: ${({ alertStatus }) => (alertStatus === AlertStatusEnum.Normal ? 'green' : 'red')};
`;
const AlertStatus = () => (
<StatusText alertStatus={AlertStatusEnum.Normal}>Custom Props of Styled Component</StatusText>
);
在 Styled Components 的官方文件中,會看到把型別定義在 styled
後,像是下方的寫法。但這文件似乎是錯誤的,可以參考 issue#3407 和這個 PR#758。
const Title = styled<{ isActive: boolean }>;
Header`
color: ${(props) => (props.isActive ? props.theme.primaryColor : props.theme.secondaryColor)}
`;
Using Styled Component
如果是想要 styled 已經寫好的 component,作法是一樣的,但要留意:
Link
元件需要接收className
來套用 styled-components 產生的樣式,且className
的型別要是 optional 的,即className?: string;
(參考 caveat with className)
// 建立 Link 元件
interface LinkProps {
text: string;
className?: string; // https://styled-components.com/docs/api#caveat-with-classname
}
const Link = ({ className, text }: LinkProps) => {
return (
<a
className={className}
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
{text}
</a>
);
};
// 將 Link Component 添加樣式後,產生 StyledLink
interface StyledLinkProps {
fontSize: string;
}
const StyledLink = styled(Link)<StyledLinkProps>`
font-size: ${(props) => props.fontSize};
`;
// 使用 StyledLink
const App = () => {
return <StyledLink text="Learn React" fontSize="24px" />;
};
使用 Theme 和 ThemeProvider
ThemeProvider 搭配 TypeScript 可以讓定義好的 theme 受到 TypeScript 型別的保護。
建立 styled.d.ts
首先要 extends StyledComponent 內建的 DefaultTheme
,因此在 src
資料夾中建立 styled.d.ts
的檔案,在裡面定義好所需的 theme 和型別:
// styled.d.ts
import 'styled-components';
declare module 'styled-components' {
export interface DefaultTheme {
borderRadius: string;
colors: {
main: string;
secondary: string;
};
}
}
建立 theme
接著建立要放入 ThemeProvider
中的 theme:
// theme.ts
import { DefaultTheme } from 'styled-components';
// 這裡會指定型別為剛剛擴展過的 DefaultTheme
const theme: DefaultTheme = {
borderRadius: '5px',
colors: {
main: 'cyan',
secondary: 'magenta',
},
};
export { theme };
使用 ThemeProvider
接著一樣使用 ThemeProvider:
import App from './App';
import { ThemeProvider } from 'styled-components';
import theme from './theme';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>,
);
使用 typeof operator 讓 TypeScript 自動推斷 theme 的型別
如果覺得需要再 styled.d.ts
中把所有 theme 都預先定義好太過麻煩的話,可以參考 Styled Components & TypeScript 的做法,利用 typeof
運算子來讓 TypeScript 自動推斷 theme 的型別:
// styled.d.ts
import 'styled-components';
import theme from './theme';
/**
* Typing the Theme
**/
declare module 'styled-components' {
// use typeof operator to auto generate type
// https://blog.agney.dev/styled-components-&-typescript/
type Theme = typeof theme;
export interface DefaultTheme extends Theme {}
}
這個做的話,theme.ts
中就不需要再匯入 defaultTheme
,改成這樣即可:
const theme = {
borderRadius: '5px',
colors: {
main: 'cyan',
secondary: 'magenta',
},
};
export { theme };
建立能接受 styled-components props 的函式
import styled, { DefaultTheme } from 'styled-components';
// 建立能接受 Styled Components Props 的 function
const handleProps = ({ $foo, theme }: { $foo: string; theme: DefaultTheme }) => {
// ...
};
const StyledComponents = styled.div<{ $foo: string }>`
// 在 styled components 中使用次 function
color: ${(props) => handleProps(props)};
`;
Create Global Style
- 透過
createGlobalStyle
可以用來建立或覆蓋全域的 CSS 樣式 - 在
GlobalStyle
中也可以使用到theme
中定義好的樣 式
// GlobalStyled.ts
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
body {
/* 覆蓋全域的 CSS 樣式 */
}
.night {
background-color: gray;
color: ${(props) => props.theme.colors.light}
}
`;
export default GlobalStyle;
使用:
// index.tsx
import { render } from 'react-dom';
import { ThemeProvider } from 'styled-components';
import theme from './theme/theme';
import App from './App';
import GlobalStyle from './theme/GlobalStyle';
const rootElement = document.getElementById('root');
render(
<ThemeProvider theme={theme}>
<GlobalStyle />
<App />
</ThemeProvider>,
rootElement,
);
Sample Code
Create Global Style @ CodeSandbox
CSS Helper
用法一:根據 props 建立不同的 CSS 樣式時
或者如果需要在 Styled Components 中根據 props 建立不同的 CSS 樣式時:
import styled, { css } from 'styled-components';
const StyledHeader = styled.div<StyledHeaderProp>`
// 直接根據 props 動態變化許多樣式
${(props) =>
props.fontSize === 'large'
? css`
`
: css`
`}
`;
用法二:建立 mixins
透過 css
helper 定義 mixing:
// mixin.ts
import { css } from 'styled-components';
interface StyledHeaderProp {
status: 'success' | 'fail';
fontSize: 'large' | 'normal' | 'small';
}
export const largeFontSize = css`
font-size: 36px;
font-weight: bold;
`;
export const normalFontSize = css`
font-size: 24px;
font-weight: normal;
`;
export const smallFontSize = css`
font-size: 12px;
font-weight: thin;
`;
可以直接在 Styled Components 中使用定義好的 mixins:
import { largeFontSize, normalFontSize, smallFontSize } from '../theme/mixins';
const StyledHeader = styled.div<StyledHeaderProp>`
// 使用定義好的 mixins
${(props) =>
props.fontSize === 'large'
? largeFontSize
: props.fontSize === 'small'
? smallFontSize
: normalFontSize};
`;
用法三:在 CSS helpers 中接收 props
有需要的話,在 css 中也可以帶入 props:
interface StyledHeaderProp {
status: 'success' | 'fail';
fontSize: 'large' | 'normal' | 'small';
}
// 在 css helper 中一樣可以接收 styled-components 傳進來的 props
const baseFontSize = css<StyledHeaderProp>`
font-size: ${(props) => (props.fontSize === 'large' ? '36px' : '24px')};
`;
// 在 styled-components 中使用定義好的 css
const StyledHeader = styled.div<StyledHeaderProp>`
${baseFontSize}
`;
VSCode
在搭配 VSCode 使用 styed-component 時,若遇到無法 "Fold All" 該程式碼區塊時,可以加上以下設定:
"[javascript]": {
"editor.formatOnSave": false,
"editor.formatOnPaste": false,
"editor.renderWhitespace": "all",
"editor.foldingStrategy": "indentation",
},
"[javascriptreact]": {
"editor.formatOnSave": false,
"editor.formatOnPaste": false,
"editor.renderWhitespace": "all",
"editor.foldingStrategy": "indentation"
},