進階用法

主題

styled-components 透過匯出 <ThemeProvider> 包裝組件來完全支援主題。此組件透過 context API 為其下的所有 React 組件提供主題。在渲染樹中,所有 styled-components 都可以訪問提供的佈景主題,即使它們位於多個層級深處。

為了說明這一點,讓我們創建我們的 Button 組件,但這次我們將傳遞一些變數作為主題。

函式主題

您也可以為 theme prop 傳遞一個函式。此函式將接收父主題,即來自樹中更高層級的另一個 <ThemeProvider>。這樣,主題本身就可以具有上下文關係。

此範例渲染了我們上面提到的主題按鈕和第二個按鈕,該按鈕使用第二個 ThemeProvider 來反轉背景和前景顏色。函式 invertTheme 接收上層主題並創建一個新的主題。

在沒有 styled components 的情況下獲取主題

透過 withTheme 高階組件

如果您需要在 styled components 之外使用當前主題(例如,在大型組件內部),您可以使用 withTheme 高階組件。

import { withTheme } from 'styled-components'


class MyComponent extends React.Component {
  render() {
    console.log('Current theme: ', this.props.theme)
    // ...
  }
}


export default withTheme(MyComponent)

透過 useContext React hook
v4

在使用 React Hooks 時,您也可以使用 useContext 在 styled components 之外訪問當前主題。

import { useContext } from 'react'
import { ThemeContext } from 'styled-components'


const MyComponent = () => {
  const themeContext = useContext(ThemeContext)


  console.log('Current theme: ', themeContext)
  // ...
}

透過 useTheme 自定義 hook
v5

在使用 React Hooks 時,您也可以使用 useTheme 在 styled components 之外訪問當前主題。

import { useTheme } from 'styled-components'


const MyComponent = () => {
  const theme = useTheme()


  console.log('Current theme: ', theme)
  // ...
}

theme prop

也可以使用 theme prop 將主題傳遞給組件。

這對於規避缺少的 <ThemeProvider> 或覆蓋它很有用。

Refs
v4

ref prop 傳遞給 styled component 將根據 styled target 提供以下兩者之一

  • 底層 DOM 節點(如果目標是基本元素,例如 styled.div
  • React 組件實例(如果目標是自定義組件,例如從 React.Component 擴展而來)
注意

使用舊版本的 styled-components(低於 4.0.0)或 React?請改用 innerRef prop

安全性

由於 styled-components 允許您使用任意輸入作為插值,因此您必須小心清理該輸入。使用使用者輸入作為樣式可能會導致在使用者的瀏覽器中評估攻擊者可以放置在您的應用程式中的任何 CSS。

此範例顯示了不良的使用者輸入如何甚至可能導致代表使用者調用 API 端點。

// Oh no! The user has given us a bad URL!
const userInput = '/api/withdraw-funds'


const ArbitraryComponent = styled.div`
  background: url(${userInput});
  /* More styles here... */
`

請非常小心!這顯然是一個虛構的例子,但 CSS 注入可能不明顯,並且會產生不良後果。某些 IE 版本甚至會在 url 宣告中執行任意 JavaScript。

有一個即將推出的標準可以從 JavaScript 中清除 CSS,CSS.escape。它在瀏覽器中的支援程度還不高,因此我們建議您在您的應用程式中使用 Mathias Bynens 的 polyfill

現有的 CSS

如果您選擇將 styled-components 與現有的 CSS 一起使用,則需要注意一些實現細節。

styled-components 會生成一個帶有 class 的實際樣式表,並透過 className prop 將這些 class 附加到 styled components 的 DOM 節點。它在運行時將生成的樣式表注入到文檔頭部的末尾。

為普通的 React 組件設定樣式

如果您使用 styled(MyComponent) 符號且 MyComponent 不渲染傳入的 className prop,則不會應用任何樣式。為避免此問題,請確保您的組件將傳入的 className 附加到 DOM 節點

class MyComponent extends React.Component {
  render() {
    // Attach the passed-in className to the DOM node
    return <div className={this.props.className} />
  }
}

如果您的現有樣式帶有 class,您可以將全局 class 與傳入的 class 組合起來

class MyComponent extends React.Component {
  render() {
    // Attach the passed-in className to the DOM node
    return <div className={`some-global-class ${this.props.className}`} />
  }
}

優先級問題

如果您將全局 class 與 styled component class 一起應用,結果可能不是您期望的。如果在兩個 class 中以相同的優先級定義了一個屬性,則最後一個 class 將獲勝。

// MyComponent.js
const MyComponent = styled.div`background-color: green;`;


// my-component.css
.red-bg {
  background-color: red;
}


// For some reason this component still has a green background,
// even though you're trying to override it with the "red-bg" class!
<MyComponent className="red-bg" />

在上面的例子中,styled component class 優先於全局 class,因為 styled-components 在運行時將其樣式注入到 <head> 的末尾。因此,它的樣式優於其他單個 classname 選擇器。

一種解決方案是提高樣式表中選擇器的優先級

/* my-component.css */
.red-bg.red-bg {
  background-color: red;
}

避免與第三方樣式和腳本衝突

如果您在不完全控制的頁面上部署 styled-components,則可能需要採取預防措施,以確保組件樣式不會與宿主頁面的樣式衝突。

最常見的問題是優先級不足。例如,考慮一個具有此樣式規則的宿主頁面

body.my-body button {
  padding: 24px;
}

由於該規則包含一個 classname 和兩個標籤名稱,因此它比此 styled component 生成的單個 classname 選擇器具有更高的優先級

styled.button`
  padding: 16px;
`

沒有辦法讓您的組件完全不受宿主頁面樣式的影響,但您至少可以使用 babel-plugin-styled-components-css-namespace 提高其樣式規則的優先級,它允許您為所有 styled components 指定 CSS 命名空間。一個好的命名空間類似於 #my-widget,如果您的所有 styled-components 都渲染在一個 id="my-widget" 的容器中,因為 ID 選擇器比任何數量的 classname 都具有更高的優先級。

一個比較罕見的問題是頁面上的兩個 styled-components 實例之間的衝突。您可以透過在程式碼包中使用 styled-components 實例定義 process.env.SC_ATTR 來避免這種情況。此值會覆蓋預設的 <style> 標籤屬性 data-styled(v3 及更低版本中為 data-styled-components),允許每個 styled-components 實例識別其自己的標籤。

標記模板字面值

標記模板字面值是 ES6 的新功能。它們允許您定義自定義字串插值規則,這就是我們能夠創建樣式化組件的方式。

如果您沒有傳遞任何插值,您的函式接收的第一個參數是一個包含字串的陣列。

// These are equivalent:
fn`some string here`;
fn(['some string here']);

一旦您傳遞插值,該陣列將包含傳遞的字串,並在插值的位置進行分割。其餘的參數將按順序排列為插值。

const aVar = 'good';


// These are equivalent:
fn`this is a ${aVar} day`;
fn(['this is a ', ' day'], aVar);

使用起來有點麻煩,但這意味著我們可以在樣式化組件中接收變數、函式或 mixin(css 輔助函式),並將其扁平化為純 CSS。

說到這裡,在扁平化過程中,styled-components 會忽略計算結果為 undefinednullfalse 或空字串("")的插值,這意味著您可以自由使用短路求值來有條件地添加 CSS 規則。

const Title = styled.h1<{ $upsideDown?: boolean; }>`
  /* Text centering won't break if props.$upsideDown is falsy */
  ${props => props.$upsideDown && 'transform: rotate(180deg);'}
  text-align: center;
`;

如果您想了解更多關於標記模板字面值的資訊,請查看 Max Stoiber 的文章:💅🏾 styled-components 背後的魔法

伺服器端渲染
v2+

styled-components 支援並行伺服器端渲染,並具有樣式表重新水合功能。基本思想是,每次在伺服器上渲染應用程式時,您都可以創建一個 ServerStyleSheet 並將提供程式添加到您的 React 樹中,該提供程式通過上下文 API 接收樣式。

這不會干擾全局樣式,例如 keyframescreateGlobalStyle,並允許您將 styled-components 與 React DOM 的各種 SSR API 一起使用。

工具設定

為了可靠地執行伺服器端渲染並讓客戶端套件順利接管,您需要使用我們的 babel 插件。它通過向每個樣式化組件添加確定性 ID 來防止校驗和不匹配。有關更多資訊,請參閱工具文件

對於 TypeScript 用戶,我們的駐場 TS 專家 Igor Oleinikov 為 webpack ts-loader / awesome-typescript-loader 工具鏈開發了一個 TypeScript 插件,它可以完成一些類似的任務。

如果可能,我們絕對建議使用 babel 插件,因為它更新最頻繁。現在可以使用 Babel 編譯 TypeScript,因此可能值得關閉 TS loader 並切換到純 Babel 實作以獲得生態系統的好處。

範例

基本 API 如下

import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';


const sheet = new ServerStyleSheet();
try {
  const html = renderToString(sheet.collectStyles(<YourApp />));
  const styleTags = sheet.getStyleTags(); // or sheet.getStyleElement();
} catch (error) {
  // handle error
  console.error(error);
} finally {
  sheet.seal();
}

collectStyles 方法將您的元素包裝在一個提供程式中。或者,您可以直接使用 StyleSheetManager 提供程式,而不是此方法。只需確保不要在客戶端使用它。

import { renderToString } from 'react-dom/server';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';


const sheet = new ServerStyleSheet();
try {
  const html = renderToString(
    <StyleSheetManager sheet={sheet.instance}>
      <YourApp />
    </StyleSheetManager>
  );
  const styleTags = sheet.getStyleTags(); // or sheet.getStyleElement();
} catch (error) {
  // handle error
  console.error(error);
} finally {
  sheet.seal();
}

sheet.getStyleTags() 返回多個 <style> 標籤的字串。將 CSS 字串添加到 HTML 輸出時,您需要考慮這一點。

或者,ServerStyleSheet 實例也有一個 getStyleElement() 方法,該方法返回一個 React 元素陣列。

如果渲染由於任何原因失敗,最好使用 try...catch...finally 來確保 sheet 對象始終可用於垃圾回收。確保僅在調用 sheet.getStyleTags()sheet.getStyleElement() 之後才調用 sheet.seal(),否則將拋出不同的錯誤。

注意

sheet.getStyleTags()sheet.getStyleElement() 只能在渲染元素後調用。因此,來自 sheet.getStyleElement() 的組件無法與 <YourApp /> 組合到更大的組件中。

Next.js

使用 Babel

基本上,您需要添加一個自定義的 pages/_document.js(如果您沒有的話)。然後複製 styled-components 的邏輯,將伺服器端渲染的樣式注入到 <head> 中。

請參閱 Next.js 儲存庫中的我們的範例,以獲取最新的使用方法範例。

使用 SWC

自版本 12 起,Next.js 使用一個名為 SWC 的 Rust 編譯器。如果您沒有使用任何 babel 插件,則應參考此範例

在此版本上,您只需添加 styledComponents: true,next.config.js 文件中的編譯器選項,並修改 _document 文件中的 getInitialProps ,如此範例所示,以支援 SSR。

應用程式目錄

對於 Next.js v13+ 中在 app/ 目錄中定義的路由,您需要將 styled-components 註冊表放在您的其中一個佈局文件中,如Next.js 文件中所述。請注意,這取決於 styled-components v6+。另請注意,使用了 'use client' 指令 - 因此,雖然您的頁面將在伺服器端渲染,但 styled-components 仍將出現在您的客戶端套件中。

Gatsby

Gatsby 有一個官方插件,可以為 styled-components 啟用伺服器端渲染。

請參閱插件頁面以獲取設定和使用方法說明。

串流渲染

styled-components 提供了一個用於 ReactDOMServer.renderToNodeStream() 的串流 API。串流實作分為兩個部分

在伺服器上

ReactDOMServer.renderToNodeStream 發出一個 styled-components 包裝的「可讀」串流。當整個 HTML 塊被推送到串流上時,如果有任何相應的樣式已準備好渲染,則樣式塊將被添加到 React 的 HTML 之前並轉發到客戶端瀏覽器。

import { renderToNodeStream } from 'react-dom/server';
import styled, { ServerStyleSheet } from 'styled-components';


// if you're using express.js, you'd have access to the response object "res"


// typically you'd want to write some preliminary HTML, since React doesn't handle this
res.write('<html><head><title>Test</title></head><body>');


const Heading = styled.h1`
  color: red;
`;


const sheet = new ServerStyleSheet();
const jsx = sheet.collectStyles(<Heading>Hello SSR!</Heading>);
const stream = sheet.interleaveWithNodeStream(renderToNodeStream(jsx));


// you'd then pipe the stream into the response object until it's done
stream.pipe(res, { end: false });


// and finalize the response with closing HTML
stream.on('end', () => res.end('</body></html>'));

在客戶端上

import { hydrate } from 'react-dom';


hydrate();
// your client-side react implementation

客戶端重新水合完成後,styled-components 將像往常一樣接管,並在重新定位的串流樣式之後注入任何其他動態樣式。

引用其他組件

注意

這是一個**特定於網路**的 API,您**無法**在 react-native 中使用它。

有很多方法可以將上下文覆蓋應用於組件的樣式。話雖如此,如果不設置一個眾所周知的目標 CSS 選擇器範例,然後讓它們可以在插值中使用,這很少容易做到。

styled-components 通過「組件選擇器」模式乾淨地解決了這個用例。每當組件由 styled() 工廠函式創建或包裝時,它也會被分配一個穩定的 CSS 類以用於定位。這允許極其強大的組合模式,而無需擔心命名和避免選擇器衝突。

一個實際的例子:在這裡,我們的 Icon 組件定義了它對父 Link 懸停的響應

我們可以將顏色更改規則嵌套在我們的 Link 組件中,但是我們必須同時考慮這兩組規則才能理解 Icon 的行為方式。

注意事項

此行為僅在 _Styled_ Components 的上下文中支援:在以下範例中嘗試掛載 B 將會失敗,因為組件 AReact.Component 的實例,而不是 Styled Component。

class A extends React.Component {
  render() {
    return <div />
  }
}


const B = styled.div`
  ${A} {
  }
`

拋出的錯誤 - Cannot call a class as a function(無法將類別作為函式呼叫) - 發生的原因是 styled component 嘗試將組件作為插值函式呼叫。

然而,將 A 包裹在 styled() 工廠中使其符合插值條件 -- 只要確保被包裹的組件傳遞 className 即可。

class A extends React.Component {
  render() {
    return <div className={this.props.className} />
  }
}


const StyledA = styled(A)``


const B = styled.div`
  ${StyledA} {
  }
`

樣式物件

styled-components 選擇性地支援將 CSS 以 JavaScript 物件而非字串的形式撰寫。當您已有現有的樣式物件,並希望逐步遷移到 styled-components 時,這特別有用。

繼續閱讀下一頁

API 參考