styled-components — React 生态最早的 CSS-in-JS 库
是什么
styled-components 是一个 React 库,用 JavaScript 的标签模板字面量把 CSS 写在组件文件里,自动生成不冲突的类名,再把样式注入到页面 <head>。
日常类比:传统 CSS 像印一大张共用贴纸——所有组件去同一张纸上撕自己那块,名字一样就互相覆盖。styled-components 像自助打印机——每个组件当场打印自己的贴纸,机器自动给每张编一个唯一编号,永远不会撞名字。
它由 babel 插件(编译期)+ 运行时 StyleSheet(浏览器期)两段组成。babel 插件给每个 styled.xxx 模板算一个稳定的短哈希,叫 componentId;运行时第一次渲染组件时把 CSS 文本拼出来、再 hash 一次、注入一个 <style data-styled> 标签到 head 里。
它解决的是 React 项目里传统 CSS 的三个老问题:类名全局冲突、组件和样式分散在两个文件、动态样式难表达伪类和媒体查询。换句话说,它把 “样式” 重新装回了 “组件” 这个盒子里。
为什么重要
- 不理解它,看不懂 2017-2023 年 React 项目里满屏的
styled.div写法 - 不理解它,会以为 “CSS-in-JS” 是抽象概念——其实就是 “用 JS 函数把 CSS 字符串变成 className” 这一件事
- 不理解它,无法解释为什么同样能力的 Emotion 抢走了 30% 市场份额
- 不理解它,无法判断新项目该不该选——v6 后已进入维护期,对 React Server Components 不友好
核心要点
- 标签模板字面量是入口——
styled.div后面跟反引号那段,本质是 JS 引擎把模板拆成静态字符串数组和动态值数组传给styled.div这个函数。函数怎么拼都行。这是 ES2015 的语法,不是 styled 自创。 - componentId 是稳定钩子——babel 插件遍历源码,给每个 styled 调用算 “基于文件路径 + 变量名” 的短哈希,比如
sc-Button-1a2b3c。SSR 时服务端和客户端必须算出一样的 hash,否则 hydration 失败。所以生产环境必装 babel-plugin。 - 运行时按需注入——组件第一次渲染才把 CSS 拼出来、hash、注入
<style>到 head。这和 inline-style 的关键区别是支持伪类、媒体查询、关键帧——这三样能力在style={{}}里都没有。 - componentId + 动态 hash 双重命名——DOM 里看到的 className 通常是两段,前一段是 babel 给的稳定 id(用于 SSR 和 DevTools 调试),后一段是 props 算出来的动态 hash(用于命中 cache)。两段合起来才是一个完整 class 名。
实践案例
案例 1:根据 props 切换底色的按钮
import styled from 'styled-components'
const Button = styled.button` background: ${props => props.primary ? '#0070f3' : '#fff'}; color: ${props => props.primary ? '#fff' : '#000'}; padding: 8px 16px; &:hover { opacity: 0.85; }`
<Button primary>Click</Button>每次渲染跑 props => ... 算出真实 CSS 字符串,hash 后看 cache 有没有;没有就注入新 class。primary=true 和 primary=false 产生两个不同的 class,DOM 里看到的 className 数 = 用到的 props 组合数。
案例 2:用 ThemeProvider 切换主题
import { ThemeProvider } from 'styled-components'const dark = { bg: '#000', fg: '#fff' }const Box = styled.div`background: ${p => p.theme.bg};`
<ThemeProvider theme={dark}><Box /></ThemeProvider>theme 通过 React Context 流到每个 styled 组件,props 函数能直接读 props.theme。这是 styled-components 的 “杀手锏”——无需 CSS 变量也能做主题切换。
案例 3:50 行写 mini styled,验证整个机制
可以照着下面的玩具实现自己跑一遍,把 SSR 和嵌套伪类先放一边,只看 “JS 函数怎么把字符串变成 className” 这个核心。
function styled(tag) { return (strings, ...fns) => (props) => { const css = strings.reduce((acc, s, i) => acc + s + (fns[i] ? fns[i](props) : ''), '') const hash = 'sc-' + simpleHash(css) if (!document.querySelector(`[data-id="${hash}"]`)) { const el = document.createElement('style') el.dataset.id = hash el.textContent = `.${hash} { ${css} }` document.head.appendChild(el) } return React.createElement(tag, { ...props, className: hash }) }}50 行复刻 80% 能力——这就是 styled-components 的真实复杂度,剩下 20% 是 SSR 提取、嵌套伪类解析、性能 cache 等工程细节。
踩过的坑
- 没装 babel-plugin → SSR hydration mismatch,DevTools 里也看不到
displayName - 在循环或函数体里写
styled.div→ 每次都新建组件,cache 全 miss,性能直接劣化 - props 函数里依赖外部多变的变量 → 哈希频繁失效,CSS 重复注入,head 里
<style>越堆越长 - 同项目里 styled-components 和 Emotion 混用 → 两份运行时同时跑,bundle 翻倍且互不复用 cache
适用 vs 不适用场景
适用:
- 老 React 项目(Next.js Pages Router、CRA、Vite SPA),团队已熟练
- 需要 “组件即样式” 心智的中小型 UI 库
- 主题切换频繁、希望 props 直接驱动样式的场景
- 把现有 CSS 大文件按组件拆分迁移的过渡期
不适用:
- 新建的 RSC 项目(Next.js App Router 默认)——styled 必须
'use client',违背 “零 JS 默认” 哲学 - 极致追求 bundle 体积的场景——Emotion 比它小一半,Tailwind 几乎零运行时
- 严格的 “编译期 CSS” 要求——选 vanilla-extract 或 Panda CSS
- 想长期跟进新特性的团队——v6 后已无新特性路线图
历史小故事(可跳过)
- 2016-09:Glen Maddern 提交第一版,正式给 “CSS-in-JS” 这个流派起了名字
- 2017:Emotion 出现,社区从此分裂成两派各自维护
- 2018-2019:Aphrodite、Glamor 等早期竞品被淘汰,市场基本剩 styled 和 Emotion 两家
- 2020-2022:MUI v5、Chakra 等大型组件库纷纷选 Emotion,styled-components 失去明星用户
- 2023 末:v6 发布,从 Flow 迁移到 TypeScript;同期 Max Stoiber 公开宣布退出维护
- 2024-2026:仓库进入维护期,只修 bug 不加新特性;新项目选型几乎不再考虑
时间线读起来像 OSS 流派兴衰的小教材——一个先发库被同代后来者抢走份额,再被下一代范式(atomic CSS、编译期 CSS)整体绕开。
学到什么
- “CSS-in-JS” 听起来神秘,本质就是 “用一个 JS 函数把字符串变成 className”
- 编译期 + 运行时双段架构是大量库的通用模板(webpack loader、babel 插件、CSS-in-JS 都是这个套路)
- 先发优势在 OSS 世界很有限——styled 早 Emotion 一年,照样被后者抢走份额
- 看到一个库进入 “维护期” 信号要敏感——不主动重构,但也不在新项目里再下注
- 标签模板字面量是被低估的语言特性——它让一个库可以原汁原味嵌入另一种语言(CSS、SQL、GraphQL 都用过这套路)
- 选样式方案前先回答 “我的项目在 RSC 边界上吗”——答案变了,最优解就变
延伸阅读
- 官方文档 styled-components.com(v6 章节最新)
- React 官方对 RSC 与样式方案的讨论文档
- “Tagged Template Literals” MDN 入门页
- Max Stoiber 关于离开维护的公开博客文 / 推特线程
- Next.js App Router 文档里
StyledComponentsRegistryworkaround 章节 - emotion、vanilla-extract、tailwind 三篇一起读,构成完整 React 样式光谱
关联
- emotion —— 同年代竞品,工程上 95% 等价但 bundle 更小、社区更活跃
- vanilla-extract —— 编译期 CSS-in-JS,零运行时,RSC 友好的下一代方案
- tailwind —— atomic CSS 范式,与 CSS-in-JS 思路相反但解决同样痛点
- react-spring —— 经常被 styled 组件包装的动画库,互补而非竞争
- shadcn-ui —— 新一代组件库,用 Tailwind 替代 styled,体现选型迁移趋势
反向链接
- emotion —— Emotion — 在 JS 里写样式,让浏览器拿到一张唯一的 className
- react-spring —— react-spring — 用真实弹簧的物理写网页动画
- shadcn-ui —— shadcn/ui — 把 React 组件从 npm 包变成”源码 + CLI 协议”
- stylex —— StyleX — 编译期把样式拍扁成原子 className 的 CSS-in-JS
- tailwind —— Tailwind CSS — 工具类优先样式框架
- vanilla-extract —— vanilla-extract — 把 CSS 写成 TypeScript,浏览器看到的却是零字节运行时