React 本身并不是一个全家桶框架。它只接管视图层,路由、状态、请求、表单、样式、测试,甚至移动端适配,都要靠周边库拼出来。很多开发者在学到组件、Props、Hooks 之后,进入真实项目时会突然发现选择太多:同一个功能至少有三四种常用库,每种库还分不同版本。这篇文章把 React 生态里常见的主流库按职责分好类,围绕实战选型、运行条件、参数边界和排错思路展开。不管你是准备搭一个新项目,还是准备面试,都可以先按这份清单把涉及面补齐。先说结论:真正决定项目骨架的,最核心是路由、状态、UI 三层;其他库很多可以按场景临时接进来,不用一开始全部学会。
1. 先看分层,才知道哪些库真正值得学
1.1 React 不是全家桶,而是“选型游戏”
Vue 官方通常会把路由、状态管理、构建工具都给你一个偏向于官方的选择。React 官方则很克制,核心只处理组件渲染和 Hook,剩下的事情留给社区。于是你在 React 项目里看到 React Router、Redux、Zustand、Antd、TanStack Query、Next.js 这些库,一点都不意外。
这个区别直接决定了学习方式。你不能像学 Vue 那样“学会一套全家桶就完事”,而是要先把生态分成几层,知道每层解决什么问题,再按项目需求组合。很多面试题也在考这个:给你一个需求,你会选哪些库,为什么。
1.2 主流库的职责地图
按功能划分,当前 React 生态里绝大多数常用库都落在下面这些层:
| 职责层 | 常见主流库 | 适用场景 |
|---|---|---|
| 路由 | React Router、TanStack Router、Next.js Router | 页面跳转、嵌套路由、动态路由、404 兜底 |
| 状态 | Redux Toolkit、Zustand、Jotai、MobX | 跨组件共享数据、用户登录信息、主题配置 |
| UI 组件 | Ant Design、Material UI、Arco Design、Semi Design | 减少样式和交互组件开发量 |
| 样式 | Tailwind CSS、CSS Modules、styled-components | 布局、主题、样式组织 |
| 数据请求 | Axios、TanStack Query、SWR | 接口调用、缓存、重试、失效 |
| 表单 | React Hook Form、Formik | 收集表单数据、校验、错误提示 |
| 校验 | Zod、Yup | 运行时数据校验 |
| 测试 | Jest、Vitest、Testing Library、Cypress、Playwright | 组件测试、端到端测试 |
| 跨端 | React Native、Expo | 一套 React 代码跑 iOS 和 Android |
这张图不需要背,但它能帮你建立判断标准:当需求落到某一层时,先知道该去哪一类库里找方案。真正决定项目骨架的是前三层:路由、状态、UI。后面的请求、表单、测试、跨端,更多是“按需加入”。
1.3 一开始不建议把所有库都学完
我见过不少新手把每个库的文档从头看到尾,最后写项目时反而不知道从哪里下手。更有效的路径是先搭一个能跑的最小系统:Vite + React + TypeScript + React Router + Zustand + Antd。让页面能跳转、状态能共享、组件能正常展示,整个骨架就会变得很具体。
等到项目出现了真实需求,比如接口请求、表单校验、统计图表,再逐个查对应库。这个方式比“先学完再开始”高效很多,因为你先有了场景,库的每个 API 都是用来解决问题的,而不是一堆需要记忆的抽象概念。
2. 路由层:React Router 为什么是绕不开的入口
2.1 单页应用为什么必须有路由
React 默认情况下只渲染一个根组件,页面切换靠组件状态确实能做到,但你会失去浏览器地址、刷新还原和历史记录。路由库解决的正是这三件事:URL 和页面状态同步、路径参数传递、导航历史管理。没有路由,一个应用就很难像一个真正的网站,刷新后也容易回到错误页面。
最常用的是 React Router。它是一个独立路由库,不依赖后端框架,适合纯前端单页应用。如果项目用了 Next.js 这类全栈框架,就不需要再单独装 React Router,框架自带文件路由。这是理解路由层的关键边界:到底选择独立路由库还是框架路由,取决于你整个项目技术栈。
2.2 React Router 6/7 的最小用法
老版本 React Router 常见写法是在组件里直接写 Switch 和 Route。新版本主流写法是用createBrowserRouter和RouterProvider把路由表集中管理。最小示例大概是这样的:
import { createBrowserRouter, RouterProvider } from 'react-router-dom'; const router = createBrowserRouter([ { path: '/', element: <Home /> }, { path: '/user/:id', element: <UserDetail /> }, { path: '*', element: <NotFound /> }, ]); function App() { return <RouterProvider router={router} />; }path: '/user/:id'是动态路由,表示可以从 URL 里拿到id,页面刷新后地址还能保留。path: '*'是兜底路由,匹配不到页面时显示 404。建议第一次跑路由时至少先建三条:首页、详情页、404 页,能覆盖大部分基础场景。
2.3 框架路由会把路由层“吞掉”
如果你用 Next.js 或 Remix,路由来自文件目录结构。比如在pages/blog/[id].jsx创建一个文件,就自动有了/blog/:id这个路由。这种约定式路由上手快,也减少代码量,但代价是你可能只会用框架的路由,回到纯 React 项目时反而不会写路由表。
我的建议是先把 React Router 单独学一遍,再去用框架路由。因为 React Router 里面涉及的嵌套路由、路由守卫、参数获取和重定向,在其他路由方案里同样存在。理解了底层逻辑,换到任何框架都快。
2.4 路由实战中的常见坑
我平时排查路由问题会按这个顺序来:
- 页面空白但不报错,先看
BrowserRouter或RouterProvider是否包在最外层。 - 动态路由参数拿不到,确认
useParams的路径名和 URL 大小写是否一致。 - 部署到服务器后,二级页面刷新出现 404。这通常是服务器没有把请求回退到
index.html,不是 React Router 的问题。 - 嵌套路由不显示子页面,先查
Outlet是否放在了父页面组件里。
这四个点能覆盖大部分路由报错。尤其第三个,很多人在本地跑得好好的,一部署就出问题,容易误以为路由库不支持 fallback,其实只是 Nginx 或静态托管服务还缺一条 rewrite 规则。
3. 状态层:别把所有数据都塞进全局 Store
3.1 先判断项目规模,再决定引入状态库
很多状态问题用useState就能解决。一个弹窗开合、一个输入框内容、一个筛选条件,都不需要全局状态库。真正需要跨组件共享的,是登录用户信息、主题配置、多页面共用的购物车、全局查询条件这类数据。
判断标准可以从两个角度入手:第一,状态只是父子组件传递,用props和useState就够;第二,一旦发现要往上提升状态,导致中间组件大量转发 props,再考虑引入全局状态库。不要一上来就建一个庞大的 store 文件夹,那样反而增加维护成本。
3.2 Redux Toolkit 依然适合大型团队
Redux 曾经是 React 全局状态的代名词,但早期写法比较繁琐,action、reducer、dispatch 概念多。官方后来推荐 Redux Toolkit,简化了不可变更新和样板代码。对大型项目、老团队、需要严格状态流管理、审计逻辑比较多的场景,它仍然是很稳定的选择。
用 Redux Toolkit 创建一段状态大概这样:
import { createSlice, configureStore } from '@reduxjs/toolkit'; const userSlice = createSlice({ name: 'user', initialState: { name: '' }, reducers: { setName(state, action) { state.name = action.payload; }, }, }); export const store = configureStore({ reducer: { user: userSlice.reducer }, });它并不难用,但你需要理解它背后的订阅、dispatch、selector 机制。如果只是做一个小工具站,用 Redux Toolkit 会产生不少文件,反而拖慢进度。
3.3 Zustand 是更轻量的常见选择
Zustand 是最近使用热度上升很快的状态库。它的特点是文件少、API 直接、不需要 Provider 包裹整个应用。一个典型案例:
import { create } from 'zustand'; export const useSearchStore = create((set) => ({ keyword: '', setKeyword: (value) => set({ keyword: value }), }));组件里用useSearchStore((s) => s.keyword)读取,用setKeyword修改,没有多余模板。对于中小型项目、组件库内部状态、以及不希望维护复杂 action 层的团队,Zustand 很合适。Jotai 也是一种轻量方案,它把状态拆成多个原子,适合你会频繁改变数据结构的情况。但总的来说,Zustand 已经能覆盖大多数全局状态需求。
3.4 客户端状态和服务端状态不要混在一起
这一点比选哪个库更重要。登录用户、主题、页面勾选状态,属于客户端状态,用 Zustand 或 Redux 没问题。用户列表、文章详情、订单记录,属于服务端状态,数据来自接口请求。如果把它们也塞进全局 store,代码里很容易出现“请求成功后调 setStore”这种重复逻辑。
更合理的做法是:服务端状态交给 TanStack Query 或 SWR 管。它们有自己的缓存、重试和失效机制。你会发现,只要把服务端数据从全局状态里剥离开,状态层的复杂程度会下降很多。
4. UI 组件库:后台和 C 端的选择逻辑完全不同
4.1 后台项目优先看企业级组件库
国内做中后台系统,最常见的选择是 Ant Design。它组件很全,表格、表单、日期选择、树形控件、省市区级联选择都有。很多人搜索“react 省市查询组件完整代码”,其实就是要省市区这种能力。Ant Design 的 Cascader 就能配合后端省市区数据使用,不需要自己重新写一套。
类似的选择还有字节的 Arco Design、腾讯的 Semi Design。它们风格更轻,表格和表单组件也比较完善。如果项目是后台管理系统、数据分析平台、低代码平台,我会建议优先从这三类里选一套,而不是从零写组件。背后原因很简单:后台业务对交互一致性要求高,成熟组件库已经处理好了键盘操作、无障碍、边界场景和大量浏览器兼容问题。
4.2 C 端项目更看重组件定制程度
活动页、官网、移动端 H5 页面,往往需要更多视觉定制。这时候 Material UI、Mantine 这类组件库更合适,因为它们支持主题定制,也方便覆盖样式。如果页面视觉要求非常高,很多团队干脆不用重型组件库,只保留无样式组件,比如 Radix UI、Headless UI,样式完全由设计系统决定。
判断标准是看项目目标:要快速搭出后台,就选组件完整的重型库;要精细打磨用户端体验,就选样式可覆盖能力强的库,或者干脆只用基础组件加 Tailwind。这里没有绝对的好与坏,重点是匹配场景。
4.3 Tailwind CSS、CSS Modules 和 CSS-in-JS 怎么搭配
样式方案之间并不互相排斥。Tailwind CSS 用原子类方式写样式,省去命名烦恼,适合快速布局。CSS Modules 是传统 CSS 加局部作用域,和组件绑定直接,比较容易上手。styled-components 把样式写到组件里,适合主题切换,但运行时会有额外开销。
我的建议是:新项目先定一个原则,后台项目用组件库自带样式优先,再按需引入 Tailwind 或 CSS Modules;用户端项目可以先从 Tailwind 或 CSS Modules 开始,避免全局类名互相覆盖。你不需要一开始就掌握所有样式方案,能在一个项目里把一个方案用熟,比到处换库更有价值。
4.4 图表、拖拽、富文本这类垂直需求
垂直库不需要一开始学,等需求出现时再查对应方案。图表方向常用 ECharts 封装版、Ant Design Charts、Recharts;拖拽方向常用 dnd-kit、react-dnd;富文本方向有 wangeditor、slate、tiptap。选这些库时,我通常会先看三个条件:项目是否还在维护、是否兼容当前 React 版本、是否支持我需要的格式和交互。
实际经验里,很多兼容性问题不是库本身不好,而是版本和脚手架不匹配。拿到一个垂直库,先建一个最小示例,确认它能跑通,再接入业务数据。这比直接上生产代码稳妥很多。
5. 数据请求:从 Axios 封装到服务端状态管理
5.1 Axios 还是 fetch
Axios 是目前最常用的基础请求库。它把请求拦截、响应拦截、超时、取消请求、错误码统一处理都封装好了。原生 fetch 在现代浏览器也够用,但需要自己处理拦截逻辑。如果项目结构简单,直接封装一个request函数也没问题。但一旦团队多人协作,为了统一登录失效、错误提示、loading 状态,用 Axios 通常更省事。
一个最小封装大概是这样:
import axios from 'axios'; const request = axios.create({ baseURL: '/api', timeout: 10000 }); request.interceptors.response.use( (response) => response.data, (error) => { // 统一处理 401、超时、网络错误 return Promise.reject(error); } );不建议在接口规范和错误码都没定下来时,就让每个页面直接写 fetch。那样后端一改返回结构,你就得把所有页面改一遍。
5.2 TanStack Query 和 SWR 解决什么问题
很多项目把服务端数据存进全局 store,再手动管理 loading、error、重新请求和分页。这个做法会产生大量重复代码。TanStack Query 和 SWR 是专门做服务端状态管理的库,它们帮你把请求缓存、重试、失效、窗口聚焦重新请求都处理好。
TanStack Query 的基础用法:
const { data, isLoading, error } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), });queryKey是缓存标识。当userId变化时,它会自动触发新请求。请求成功后,数据会被缓存下来,其他组件使用相同 key 时可以直接读缓存。这个机制比手动设置 store 要整洁很多。
5.3 接口层设计要注意什么
我排查请求问题时有一个固定顺序:
- 先看请求有没有发出,打开 Network 面板。
- 再看响应结构,确认状态码、业务码和消息字段。
- 然后看有没有重复请求或取消请求。
- 最后看缓存失效有没有做好。
很多“数据改了但页面没变化”的问题,不是组件代码写错,而是没做缓存失效。使用 TanStack Query 时,在新增、修改、删除成功后,要调用invalidateQueries让对应列表重新请求。如果你发现代码里到处都是“请求完成后 setStore”,说明还没有把服务端状态和客户端状态分开。
6. 表单与校验:React Hook Form 和 Zod 的组合
6.1 表单失控的常见原因
React 表单的核心难点在于字段一多,整个页面可能频繁重新渲染;校验规则多了以后,错误提示和值容易对不上;提交时还要把嵌套结构转换成接口要求的格式。简单的表单用useState就能写,但一旦出现联动校验、动态增删字段、编辑页预填,手写就会变得很乱。
这也是为什么表单库始终有人需要。表单库帮你收集字段值、触发校验、显示错误、处理提交流程,让你把精力集中在业务规则上,而不是一遍遍地写onChange。
6.2 React Hook Form 还是 Formik
React Hook Form 是目前更常见的选择。它通过register或Controller收集表单值,减少不必要的重新渲染。Formik 也是一个很经典的方案,API 直白,资料也多,但性能和维护成本没有 React Hook Form 轻。
React Hook Form 的基础用法:
import { useForm } from 'react-hook-form'; const { register, handleSubmit, formState: { errors } } = useForm(); function onSubmit(values) { console.log(values); } <form onSubmit={handleSubmit(onSubmit)}> <input {...register('name', { required: true })} /> {errors.name && <p>请输入姓名</p>} <button>提交</button> </form>如果项目已经开始用 Formik,不需要急着迁移。新项目可以优先考虑 React Hook Form。
6.3 用 Zod 把运行时校验提前
Zod 是一个运行时校验库,非常适合和 React Hook Form 配合。它可以在数据进入组件前就完成校验,也能在接口返回数据时验证结构。表单提交时,前端拿到的值已经是校验过的,格式更可控。
组合方式大概是这样:
import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const schema = z.object({ email: z.string().email('邮箱格式不正确'), password: z.string().min(6, '密码至少6位'), }); const { register, handleSubmit } = useForm({ resolver: zodResolver(schema) });前端校验规则就集中在schema里,避免了在组件各处写 if/else。当表单项多、接口字段复杂时,这个组合会把开发成本压下去。
7. 质量保障:组件测试到底该测什么
7.1 组件测试测试的是用户行为
组件测试不是把所有内部函数都测一遍。最值得测的是用户操作后的结果:点击按钮是否触发事件、输入内容后页面是否更新、请求失败时是否展示错误提示。Testing Library 的核心思想是“像用户一样使用组件”,它鼓励你去检查页面内容,而不是测试内部状态值。
一个典型测试:
import { render, screen, fireEvent } from '@testing-library/react'; test('点击按钮后显示文字', () => { render(<Button />); fireEvent.click(screen.getByText('点击')); expect(screen.getByText('已点击')).toBeInTheDocument(); });这样写出来的测试更像在描述需求,而不是绑定实现细节。以后重构组件内部逻辑时,测试不容易跟着失效。
7.2 Jest、Vitest、Testing Library、Cypress 怎么搭配
Jest 是 React 生态里很常见的测试框架,老项目里出现概率很高。新项目有越来越多团队使用 Vitest,因为和 Vite 集成好、启动快。最小组合是测试框架加 Testing Library,用于组件渲染和交互行为。端到端测试用 Cypress 或 Playwright,用来验证用户完整路径,比如登录、跳转、提交表单。
测试框架的选择会受构建工具影响。项目用 Vite,就优先考虑 Vitest;项目是老式 webpack,Jest 可能更稳。不要为了“新”去强行换测试框架,先看项目当前构建方式。
7.3 测试成本与产出怎么平衡
不是所有组件都要写测试。我建议优先给这四类内容写:页面跳转逻辑、表单校验规则、接口失败态、核心业务组件。纯展示组件和简单静态页面,测试成本高、收益低,可以暂时不写。
把测试当成保障措施,不要当成覆盖率数字游戏。实际项目中,最高的风险往往出现在业务状态变化、接口返回异常、权限控制这些地方。把测试集中在那里,比追求 100% 覆盖率更有效。
8. 跨端路线:React Native 最常见的问题
8.1 React Native 到底是什么位置
React Native 是 React 的移动端方案,让同一套 React 代码运行在 iOS 和 Android 上。它不是一个单纯 UI 库,而是一整套运行时环境,包含 Metro 打包器、原生模块、Android Studio 或 Xcode 工程。很多人搜索“react native 统计图”“react native 启动白屏”,说明真实项目里最常见的问题不是“能不能用”,而是环境、依赖和原生模块不兼容。
8.2 启动白屏的入手排查顺序
React Native 启动白屏,我建议按这个顺序排查:
- 看 Metro 终端。白屏时终端里没有包编译完成记录,一般来说是开发服务器没连上。
- 看 JS 首屏代码。在入口组件加个
console.log或try/catch,确认执行到了哪一行。 - 看原生依赖。Android 端只要新增原生模块,就必须重新构建 native 工程,否则运行时可能挂掉。
- 清缓存重启。常见命令是:
npx react-native start --reset-cache然后再重新运行npx react-native run-android或run-ios。大部分白屏问题都能按这个链路定位到。
8.3 Android Studio 和 Expo 怎么选
如果你要用 React Native 跑 Android,需要安装 JDK、Android Studio、配置 Android SDK 路径,还要设置ANDROID_HOME环境变量。这个过程相对繁琐,很多人卡在 Gradle 下载、SDK 版本和模拟器启动上。如果只是快速验证一个 React Native 项目,可以先使用 Expo。Expo 简化了原生构建流程,通过 Expo Go 扫二维码就能跑,不用先折腾 Android Studio。
需要注意:Expo 适合纯 JS 功能和内置模块。如果要接入自定义原生 SDK、蓝牙、特殊支付等能力,可能需要弹出原生工程,回到完整 React Native 开发。所以先