跳至主要内容

[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" />;

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>&lt; 💅 &gt;</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`
font-size: 36px;
font-weight: bold;
`
: css`
font-size: 24px;
font-weight: normal;
`}
`;

用法二:建立 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}
`;

CSS Helper

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"
},

參考文章