
一句话:选型不是「哪个技术更好」,而是「在给定约束下,哪个技术的『代价』你更愿意承担」——所以每个选型都该回答五个问题:约束是什么、候选有哪些、判据是什么、代价是什么、退出策略是什么。
一、选型的通用框架(⭐ 五问)
⭐⭐⭐ 任何选型都过这五问(⭐ 「退出策略」是最容易被忽略的一问):
① 【约束是什么】⭐
· 团队规模与经验(⭐ 「3 个新人」与「10 个老手」完全不同)
· 项目周期(⭐ 「3 个月的营销页」与「要维护 5 年的后台」不同)
· 性能/可访问性/合规的硬要求
· 「已有技术栈」与「迁移成本」
⭐ 不问约束就选型 = 「拿「别人的答案」解「自己的题」
② 【候选有哪些】⭐
⭐ 「至少列出 2 个真实候选」(⭐ 只想到一个说明「见得不够」)
③ 【判据是什么】⭐
⭐ 把「感觉」变成「可验证的判据」:
· 「体积 < 20KB?」「类型安全?」「SSR 支持?」「团队熟悉度?」
· ⭐ 「社区活跃度」(⚠️ 「一年没发版」= 未来的坑)
⚠️ 判据要「能排序」而不是「都很重要」
④ 【代价是什么】⭐ 最容易被跳过的一问
⭐ 每个技术都有代价:「学习成本」「运行时开销」「锁定」「生态迁移成本」
⭐ 「没有代价的选项」通常意味着「你还没看出它的代价」
⑤ 【退出策略是什么】⭐
⭐ 「如果用了一年发现不对,怎么换?」
· ✅ 「有边界」的(如「数据层封装在一个 `api/` 目录」)→ 好换
· ❌ 「渗透到每个组件」的(如「CSS-in-JS 的写法散落各处」)→ 难换
⭐ 判据:「**这个技术会被「隔离在一个层里」还是「渗透到业务代码里」**」
二、八个高频选型
选型 1:状态管理
| 候选 |
什么时候选 |
什么时候不选 |
useState + 提升 + Context |
⭐ 「状态少、共享范围小」(多数场景) |
「跨多个不相邻组件」「高频更新」(⭐ Context 会全量重渲染,见 5.9) |
| ⭐ Zustand |
⭐ 「需要全局状态 + 想要简单」(⭐ 默认推荐);「需要在 React 外读写」 |
「需要严格的时间旅行/事件溯源」(⭐ Redux 更合适) |
| ⭐ Jotai |
⭐ 「派生值多、依赖关系复杂」(表格合计、多条件联动,见 3.11) |
「团队不熟悉原子化模型」(⭐ 学习成本真实存在) |
| Redux Toolkit |
⭐ 「大团队 + 需要严格可追溯/中间件生态/时间旅行」 |
「中小项目」(⭐ 样板代码与心智成本偏高) |
| MobX / Valtio |
⭐ 「深层嵌套 + 大量就地修改」(表单、树、编辑器,见 3.12) |
「团队不接受「代理式响应式」的调试方式」 |
| XState |
⭐ 「状态多 + 有非法组合风险 + 有异步与超时」(支付、多步向导,见 3.13) |
「状态少、转移简单」(⭐ 杀鸡用牛刀) |
⭐⭐⭐ 「状态管理选型」的四个判据(⭐ 按顺序问自己):
① ⭐ 「这个状态需要「跨组件共享」吗」—— 不需要 → `useState`(⭐ 别上库)
② ⭐ 「它是「服务端状态」还是「客户端状态」」—— 服务端 → **数据层**(见选型 2),
不是「状态管理库」
③ ⭐ 「共享范围多大」—— 「一个子树」→ Context/局部 store;「全局」→ 全局 store
④ ⭐ 「更新频率多高」—— 高频 → ⭐ **必须「细粒度订阅」**(Zustand 选择器 / Jotai 原子)
⚠️ 「Context 在高频场景会全量重渲染」
⭐ 一句话:「**先问「要不要」,再问「用哪个」**」——
⚠️ 「为了一个弹窗开关引入状态库」是常见过度设计。
选型 2:数据层(服务端状态)
| 候选 |
什么时候选 |
什么时候不选 |
| ⭐ TanStack Query(React Query) |
⭐ 「REST/GraphQL 都要」「需要缓存/失效/乐观更新/分页/轮询」(⭐ 默认推荐,见 7.4) |
「只是「进页面拉一次」」(⭐ 可能不需要) |
| SWR |
⭐ 「简单场景 + 体积敏感」(⭐ 更小、API 更少) |
「需要「无限查询/复杂缓存操作」(⭐ 功能少一些) |
| Apollo / urql |
⭐ 「用 GraphQL + 需要「规范化缓存」」(见 7.10) |
「不是 GraphQL」或「不需要「跨查询自动更新」」 |
| ⭐ tRPC |
⭐ 「全栈 TS 同仓」(⭐ 端到端类型安全,见 7.11) |
「需要给「外部/多语言客户端」提供 API」 |
自己封装 fetch 层 |
⭐ 「需求很简单」或「需要「极致的定制」」 |
⚠️ 「你很快就会「重新实现一遍 React Query 的坑」(⭐ 缓存/竞态/失效) |
| 框架内建(Next.js loader/RSC) |
⭐ 「用 Next.js 且「数据与路由强相关」」(见 6.11/9.9) |
「需要「客户端侧复杂的缓存与失效」」(⭐ 那时还是配 React Query) |
⭐⭐⭐ 「数据层」最容易搞错的三件事:
① ⭐ **「服务端状态」不是「客户端状态」**(见 3.10)——
⚠️ 「把接口数据塞进 Redux 并自己管 loading/error/缓存」
= 「重新发明一个「更差的 React Query」
② ⭐ **数据层「与路由层的分工」**(见 6.11/9.9)——
Next.js 的 loader/RSC 可以做「路由级数据」,但「客户端复杂缓存」还是要数据层
③ ⭐ **「缓存策略」要有「失效设计」**——
⚠️ 「配了缓存但没想「什么时候失效」」→ 「用户看到旧数据」(见 9.9)
选型 3:样式方案
| 候选 |
什么时候选 |
什么时候不选 |
| CSS Modules |
⭐ 「要「零运行时 + 传统 CSS 能力」」 |
「需要「极致的动态样式」(主题切换靠 CSS 变量仍然可以) |
| ⭐ Tailwind |
⭐ 「团队接受「原子类」+ 想要「快速迭代 + 一致的设计尺度」」(⭐ 配合 token) |
「设计稿与类名体系差异大」或「团队讨厌「HTML 里一长串类名」」 |
| CSS-in-JS(运行时,如 emotion) |
「需要「基于 props 的动态样式」」 |
⭐ 「性能敏感」(⭐ 运行时开销,见 10.8);「组件库」(⭐ 优先构建期方案) |
| vanilla-extract / Linaria |
⭐ 「想要「类型安全的 CSS」+ 零运行时」 |
「团队不想引入「构建期方案」的复杂度」 |
| Sass/Less |
⭐ 「已有资产 / 需要「复杂嵌套与函数」」 |
「新项目」(⭐ 现代 CSS 变量 + @layer 已覆盖大部分需求) |
⭐⭐⭐ 「样式选型」的三个判断点:
① ⭐ **「运行时的成本」**——「运行时 CSS-in-JS」每渲染一次都要「生成/插入样式」
(⭐ 见 10.8 的对比表)→ ⭐ 「组件库/性能敏感」优先「构建期方案」
② ⭐ **「主题切换」怎么做**——⭐ **CSS 变量是最通用的解**(见 10.8):
「构建期方案 + CSS 变量」= 「零运行时 + 可换肤」
③ ⭐ **「设计一致性」**——「Tailwind 的「尺度约束」本身就是「设计系统的载体」」
(⚠️ 「自由写 CSS」更容易「像素漂移」,见 10.13)
选型 4:表单
| 候选 |
什么时候选 |
什么时候不选 |
| ⭐ react-hook-form |
⭐ 「字段多 / 需要性能」(⭐ 非受控 + 字段级订阅,见 1.13) |
「只有 1~2 个字段」(⭐ useState 就够) |
| TanStack Form |
⭐ 「想要「类型推导最强」+ 与 TanStack 生态一致」 |
「团队不熟悉 / 生态较新」 |
| Formik |
⚠️ 「老项目在维护」(⭐ 新项目一般不再选:受控 + 全量重渲染) |
⭐ 「新项目」(见上) |
| 自己写 |
⭐ 「需求极简」或「要「完全控制」」 |
⚠️ 「字段一多就会「重新发明一遍表单库」(⭐ 竞态/校验时机/数组字段) |
⭐ 判据:⭐ **「字段数」与「是否需要「复杂校验」」**
· 「≤ 5 个字段 + 简单校验」→ ⭐ 自己写(`useState` + 手写校验)
· ⭐ 「字段多 / 跨字段校验 / 数组字段 / 异步校验」→ **react-hook-form**
⚠️ 且要记住它的核心纪律:「**不要展开整个 `formState`**」(见 1.13)
选型 5:路由
| 候选 |
什么时候选 |
什么时候不选 |
| ⭐ React Router |
⭐ 「生态最主流 + 团队熟悉」(⭐ 默认推荐) |
「重度依赖 URL 状态的搜索页/后台」(⭐ 见下) |
| ⭐ TanStack Router |
⭐ 「URL 状态复杂(筛选/分页/排序都在 URL)+ 想要类型安全」(见 6.11) |
「路由简单」或「团队不想学新东西」 |
| Next.js App Router(文件路由) |
⭐ 「用 Next.js」(⭐ 路由即文件结构 + 与 RSC 配合) |
「纯 SPA 且不需要 SSR」 |
| 自己写(history 封装) |
⭐ 「学习/极简场景」 |
⚠️ 「生产项目」(⭐ 你会「重新实现一遍「嵌套路由/懒加载/滚动恢复」」) |
⭐ 判据:⭐ **「URL 是不是「一等公民的状态容器」」**
· 「是」(电商筛选、数据看板、后台报表)→ ⭐ **TanStack Router 的收益最大**(见 6.11)
· 「否」(页面少、参数简单)→ ⭐ React Router 更省心(⭐ 生态熟悉度也是成本)
选型 6:构建工具
| 候选 |
什么时候选 |
什么时候不选 |
| ⭐ Vite |
⭐ 「SPA/库开发」——开发体验最好(⭐ 见 10.14 的两段式机制) |
「需要「SSR/RSC/文件路由」的一体化」(⭐ 那是框架的活) |
| ⭐ Next.js(next build) |
⭐ 「需要 SSR/SSG/RSC/API」的全栈应用 |
「纯静态 SPA」(⭐ 可能过度) |
| Rspack |
⭐ 「已有 webpack 资产 + 想要大幅提速」(⭐ API 兼容,见 10.14) |
「新项目」(⭐ Vite 通常更省心) |
| webpack |
⚠️ 「老项目在维护 / 深度定制的构建」 |
「新项目」(⭐ 慢、配置多) |
| Turbopack |
⭐ 「Next.js 项目想更快」(⭐ 逐步成熟) |
⚠️ 「求稳的生产环境」(⭐ 视版本成熟度) |
⭐ 判据:⭐ **「你要不要「框架能力」(SSR/RSC/文件路由)」**
· 「要」→ ⭐ **用框架**(Next.js 等)——构建工具是它的内部实现
· 「不要」(纯 SPA)→ ⭐ **Vite**
· 「已有 webpack 且很痛」→ ⭐ **Rspack 是「迁移成本最低的提速」**
选型 7:测试
| 候选 |
什么时候选 |
什么时候不选 |
| ⭐ Vitest |
⭐ 「新项目 / Vite 项目」(⭐ 快、配置少、API 兼容 Jest) |
「已有大量 Jest 配置与生态依赖」 |
| Jest |
⭐ 「老项目 / 依赖 Jest 生态(如某些 React Native 配置)」 |
⭐ 「新项目」(⭐ Vitest 更快更省心) |
| ⭐ Testing Library |
⭐ 「组件测试」的标准(⭐ 且「用 role 查询顺便校验无障碍」,见 10.12) |
— |
| ⭐ Playwright |
⭐ 「E2E + 跨浏览器 + 分片」(⭐ 默认推荐,见 10.14) |
「只测「单页小组件」」(⭐ 单测更合适) |
| Cypress |
⭐ 「团队已熟悉 / 喜欢它的 DX」 |
⚠️ 「跨浏览器 + 并行分片的成本」(⭐ Playwright 更省心) |
| Storybook + 视觉回归 |
⭐ 「组件库/设计系统」必备(⭐ 见 10.8) |
「简单业务项目」(⭐ 成本可能不划算) |
⭐⭐⭐ 「测试选型」的核心是「**分层**」(⭐ 不是「选一个」):
① ⭐ **单测(Vitest)**:纯逻辑、工具、reducer、service(⭐ 最多、最快)
② ⭐ **组件测试(Testing Library)**:组件行为(⭐ 用 `role` 查询)
③ ⭐ **E2E(Playwright)**:核心用户流程(⭐ 少而精,覆盖「下单/支付/登录」)
⭐ 比例建议:「**大量单测 + 适量组件测试 + 少量 E2E**」(⭐ 反过来的「冰淇淋筒」是反模式)
选型 8:组件库 / 设计系统
| 候选 |
什么时候选 |
什么时候不选 |
| Ant Design |
⭐ 「中后台」(⭐ 组件全、表单/表格强) |
「C 端「品牌感强」的页面」(⭐ 定制成本高) |
| MUI |
⭐ 「Material 风格 + 国际化团队」 |
「设计语言与 Material 差异大」 |
| shadcn/ui |
⭐ 「想要「代码在你仓库里」+ 完全可控」(⭐ 复制式而非依赖式) |
「团队不想「自己维护组件」」 |
| Radix / Ark UI(Headless) |
⭐ 「要自建设计系统」(⭐ 无样式 + 无障碍 + 可组合,见 1.11) |
「需要「开箱即用的样式」」 |
| ⭐ 自研设计系统 |
⭐ 「有设计团队 + 长期维护 + 品牌要求」 |
⚠️ 「没有「设计系统维护者」」(⭐ 会烂掉,见 10.8) |
⭐⭐⭐ 判据(⭐ 四问):
① ⭐ 「C 端还是中后台」—— ⭐ 中后台「用成熟库」(省时间);C 端「自研或 Headless」
② ⭐ 「要不要「自己拥有代码」」—— ⭐ shadcn/ui 的思路(「复制进仓库」)适合「想改造」
③ ⭐ 「有没有「设计系统的人」」—— ⚠️ 「没有「专职设计系统」的自研 = 「迟早烂掉」
④ ⭐ 「无障碍要求」—— ⭐ Radix/Ark 这类 Headless 库「无障碍做得好」(见 10.12)
三、选型的反模式
⭐⭐⭐ 八个「选型反模式」(⭐ 面试里能说出这些,比「背对比表」加分):
① ⭐ **「简历驱动选型」**——「用它是因为「想学/想写在简历上」
✅ 「用它的理由应该是「约束」」
② ⭐ **「最流行驱动」**——「star 最多就选它」
⚠️ 「流行度是「判据之一」但「不等于适合你的约束」」
③ ⭐ **「不为「退出」设计」**——把某个库的 API 渗透到业务代码里
✅ 「隔离在一层」(⭐ 见五问的「退出策略」)
④ ⭐ **「一次引入太多」**——「状态库 + 数据层 + 表单库 + 样式库 + UI 库」同时上
✅ 「一次引入一个,且「每引入一个都要「说明它解决了什么痛点」」
⑤ ⭐ **「重复职责」**——「Redux 管缓存 + React Query 也管缓存」
⚠️ 「两层缓存」→ 「不知道数据从哪来」(⭐ 见 7.10 的「Apollo 与 React Query 别叠」)
⑥ ⭐ **「为了「统一」而统一」**——「全公司必须用同一个 UI 库」
⚠️ 「不同产品的「约束不同」(⭐ C 端与中后台的诉求相反)
⑦ ⭐ **「忽略「团队熟悉度」」**——选一个「只有一个人懂」的技术
⭐ 「**降低认知成本往往比「技术更优」更重要**」(⭐ 招聘/交接/搜索答案)
⑧ ⭐ **「不写「决策记录」」**——一年后「没人知道「为什么选它」」
✅ ⭐ 「写 ADR(Architecture Decision Record):**背景、候选、决策、代价、何时重评**」
<!-- ⭐⭐ 一个「决策记录」模板(⭐ 团队工程化的标志) -->
# ADR-012:数据层选用 TanStack Query
## 背景
- 我们有 40+ 个接口,全部是 REST;当前「每个页面自己 useEffect + useState」。
- 痛点:缓存重复实现、竞态、失效靠手动、loading/error 状态散落。
## 候选
1. TanStack Query(缓存/失效/乐观更新/分页齐全)
2. SWR(更小,但「无限查询/复杂失效」能力弱)
3. 自己封装 fetch 层(可控但「要自己实现竞态/缓存/失效」)
## 决策
选 **TanStack Query**。
理由:① 我们「需要分页与乐观更新」(SWR 能力不足);② 社区活跃 + 团队有人用过。
## 代价
- ⭐ 新增约 12KB(gzip);⭐ 团队要理解「服务端状态与客户端状态的区别」。
- ⚠️ 要注意「不要与「Next.js 的缓存」叠加两层」(见 9.9)。
## 何时重评
- 如果「tRPC 成为全栈标准」→ 我们会「基于 tRPC 的 React Query 集成」(见 7.11)。
- 如果「接口数量降到 10 个以内」→ 考虑「去掉它」。
四、面试延伸:怎么答「选型题」
⭐⭐⭐ 「你怎么做技术选型?」(⭐ 五步答法,与开头呼应)
① ⭐ 【先问约束】
「我会先确认「团队规模与经验、项目周期、性能要求、已有栈、迁移成本」——
因为「约束不同,答案完全不同」。」
② ⭐ 【列候选 + 判据】
「然后列 2~3 个真实候选,并给「可排序的判据」:
体积、类型安全、SSR 支持、社区活跃度、团队熟悉度。」
③ ⭐ 【主动讲代价】⭐
「每个选项我都会说「它的代价」——
比如「选 Tailwind 换来「迭代速度与设计尺度一致」,代价是「HTML 里类名很长 + 团队要接受原子类」」。」
④ ⭐ 【讲退出策略】⭐ 加分项
「我会问「这个东西会「被隔离在一层」还是「渗透到业务代码」」——
能被隔离的(数据层、表单层)好换;渗透的(样式、组件库)要更慎重。」
⑤ ⭐ 【给「重评条件」】
「最后我会写下「什么情况下我们会重新评估」——
比如「如果接口数量下降、或者生态标准变了」。」
⭐ 三个「加分的话术」:
① ⭐ 「**降低认知成本往往比「技术更优」更重要**」(⭐ 团队熟悉度是真实成本)
② ⭐ 「**我会先问「现在有什么痛点」,再决定要不要引入**」
(⭐ 而不是「因为这个库很流行」)
③ ⭐ 「**我会写一个 ADR,把「代价」和「重评条件」记下来**」
(⭐ 展示「工程化思维」)
五、一句话速记
选型 = 「在给定约束下,选一个「代价你更愿意承担」的方案」——⭐ 五问框架:约束是什么(团队/周期/硬要求/已有栈/迁移成本)→ 候选有哪些(至少 2 个真实候选)→ 判据是什么(可排序:体积/类型/SSR/活跃度/熟悉度)→ ⭐ **代价是什么(最容易被跳过:学习成本/运行时开销/锁定/生态迁移)→ ⭐ **退出策略(「它会被隔离在一层还是渗透到业务」);八个高频选型的核心判据:① 状态管理(先问「要不要」再问「用哪个」:跨组件?服务端还是客户端状态?共享范围?更新频率?——高频必须细粒度订阅)、② 数据层(服务端状态≠客户端状态,别用 Redux 管接口数据;简单可以用框架 loader/RSC,复杂缓存才上 React Query)、③ 样式(⭐ **运行时 CSS-in-JS 有每渲染成本 → 组件库/性能敏感优先「构建期 + CSS 变量」)、④ 表单(≤5 字段自己写,多字段/跨字段校验/异步校验 → react-hook-form)、⑤ 路由(⭐ 判据是「URL 是不是一等公民的状态容器」→ 是就用 TanStack Router,否就用 React Router)、⑥ 构建(要框架能力用 Next.js,纯 SPA 用 Vite,webpack 资产迁移用 Rspack)、⑦ 测试(⭐ 分层:大量单测 + 适量组件测试 + 少量 E2E;E2E 优先 Playwright)、⑧ 组件库(⭐ C 端自研/Headless、中后台用成熟库;没有专职设计系统就别自研);⭐ **八个反模式:简历驱动 / 最流行驱动 / 不为退出设计 / 一次引入太多 / 重复职责(两层缓存)/ 为统一而统一 / 忽略团队熟悉度 / ⭐ 不写决策记录(ADR:背景·候选·决策·代价·何时重评);⭐ 面试加分话术:「降低认知成本往往比技术更优更重要」「先问现在有什么痛点」「写 ADR 记下代价与重评条件」。
最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!