“rea”这个标题我第一次看到的时候说实话愣了一下。三个字母没有后缀没有上下文完全不像一个正经项目名。但做前端做得久了这种缩写反而见得多——有人拿它当文件夹名有人拿它当React的简写还有人纯粹是想给项目起个短名字。结合“最新网络热词”这个信息我倾向于把“rea”理解为React技术栈相关的一次实践记录。如果你正好在学React或者打算用React重构一个老项目这篇东西应该能帮你少踩几个坑。React这个框架在前端圈子里火了很多年到现在依然是招聘市场上出现频率最高的关键词之一。但真正上手之后你会发现会写组件和能写好组件是两码事状态管理乱成一团、组件拆分不合理、闭包陷阱防不胜防、依赖数组改到想骂人。这篇文章不打算从头教JSX语法而是围绕一次真实的React项目实践把我拆解设计思路、梳理核心细节、敲代码、排坑的全过程记录下来。适合刚学会React基础、想动手做点东西的开发者也适合那些已经写过几个页面但总觉得代码乱、想理清思路的人。1. 从“rea”说起项目命名的逻辑与定位1.1 名字背后的真实含义很多开发者习惯用三个字母给项目起名原因其实很简单命令行里敲起来快创建文件夹省事而且带点“代号”的仪式感。我这次做的这个项目内部代号就是“rea”。它在语义上可以理解成React的缩写也可以看作一个空壳名字等后续再换。实际开发中这种情况非常常见名字只是一个标识代码质量和架构设计才是真正值钱的部分。不过名字虽然是临时的项目定位可不能含糊。“rea”这个项目要解决什么问题我在动手前想了很久。答案是我想做一个内容管理后台的Demo用来验证一套React组件复用方案。说白了就是拿真实需求练手把那些平时容易忽略的边界情况全部跑一遍。1.2 我为什么选择React而不是其它方案在框架选型上我几乎没有犹豫。项目需要的是一个稳定的UI层同时组件复用频率很高React的函数组件加Hooks模式正好契合。对比Vue的模板语法React的“一切皆JavaScript”风格在复杂交互场景下更自由调试起来也更直接。另一个考虑是生态。无论是路由、状态管理还是UI组件库React周边方案最齐全踩坑的时候能搜到的参考资料也最多。对个人项目来说这意味着迭代速度可以更快。而且这次项目里的几个关键功能——表格、表单、弹窗、权限路由——React生态里都有非常成熟的搭配。1.3 这个项目到底适合谁参考我写这篇记录时一直在想什么人来读会最有用。如果你是那种刚刚看完React文档、能写Hello World但对Hooks还一知半解的初学者这篇文章里的每个思路和代码片段都可以直接抄。如果你已经在公司写过一两个后台项目但对组件拆分、状态管理还是凭感觉来那篇文章里的设计逻辑能帮你建立一个更清晰的判断标准。2. 先设计再动手组件拆分与状态管理思路2.1 组件拆分的边界到底怎么划很多人写React项目最大的问题就是组件粒度没把握好。要么一个文件写了上千行要么拆出几十个只有三行的空壳组件结果代码比不拆还难维护。我自己的标准很简单**一个组件只做一件事且这件事必须要被复用两次以上才会拆。**如果只是某个表单里的一个输入框那直接写在当前组件里加个注释就好如果这个输入框在三个页面里都出现那就必须抽成独立组件。这次项目里我拆了大概十几个组件核心的几个包括表格容器、筛选栏、抽屉表单、通用弹窗、详情页骨架。每个组件都是按“是否会被复用”这个维度决定的没有提前做过度设计。有一个细节值得说我开始时给表格容器设计了十几个属性后来真正用起来只有一半。经验就是组件属性设计应该从实际调用场景倒推而不是从“组件可能需要什么”的正向思维去堆。2.2 状态放哪里本地还是全局React状态管理大概是每个项目里争论最多的问题。这次我的策略是分三层UI临时状态比如弹窗是否显示放组件内部用useState跨组件共享但不跨模块的状态比如当前选中的表格行用Context需要持久化或模块间共享的数据比如用户信息、权限列表直接用状态管理库这次用的是Zustand。这个分层方案很多人可能觉得没什么稀奇但它保证了代码不会乱。具体到每个状态我还会问一句这个数据改动后哪些组件需要感知如果只有一个组件需要感知那就绝不放全局这是最简单的判断标准。2.3 目录结构扁平还是分层目录结构这种事我踩过好几次坑。曾经按“components、pages、utils、services”这种纯类型分类结果找代码和猜谜一样。现在用的方案是按功能域分组src/ api/ components/ layout/ common/ business/ hooks/ pages/ dashboard/ user/ settings/ store/ utils/这套结构的核心逻辑是components下面再按业务域细分而不是把所有按钮、弹窗都堆在一个文件夹里。Pages按页面域划分每个页面域里自己管理子组件。这样新增功能时改动的代码几乎都在同一个文件夹里心智负担小很多。3. 核心细节拆解Hooks、数据流与副作用处理3.1 Hooks不是会用就行关键是理解触发机制useState、useEffect、useMemo这些Hooks文档里每个都写了“什么情况怎么用”但我见过太多人写出问题代码根源在于没理解一个本质**Hooks的执行是跟着组件渲染走的而不是跟着事件走的。**比如useState它的值在被set后组件会重新渲染但函数体里的普通变量可不会因为数据变化重新赋值。很多人在这里犯迷糊写出了“数据明明改了变量还是旧值”的bug。一个小技巧是如果你在函数组件里写了一个普通变量来保存实时数据那它一定不会实时变化。想要“实时”的效果要么把值放进state要么用useRef。useState用于触发渲染useRef用于在渲染之间保留引用而不触发渲染这个区别非常重要。3.2 子父组件通信的三种姿势组件通信是React项目里绕不开的问题。这次项目里我试过三种方式分别对应不同场景第一种父组件通过props向子组件传递数据和方法。这是最基础的通信方式简单直接但层级一深就会出现“逐层传递”的问题组件越多代码越长。我在这次项目里只保留了一层或两层的props传递超过两层我会考虑换方式。第二种通过回调函数让子组件反向通知父组件。本质上还是props但模式上有区别子组件的操作不应该直接修改父组件的数据而是调用父组件传下来的方法由父组件自己决定改什么。这样数据流向是单向的排查问题的时候顺着方向找就能定位。第三种Context或全局状态适合需要跨越多个层级的共享数据。比如这次项目里的主题配置、当前登录用户信息如果一层层传props下去改一个字段得动五六个文件。用Context之后任何层级的组件都能直接读取代码清爽很多。3.3 闭包陷阱最常见的React隐性bug闭包陷阱是我这次项目里遇到最多的问题也是最难调试的问题之一。场景是这样的在一个事件处理函数里读取了某个state的值但这个值是在事件绑定那一刻快照下来的等事件真正触发时组件可能已经重新渲染了好几次state已经变了但闭包里的值还是旧的。最简单的演示代码function Counter() { const [count, setCount] useState(0); useEffect(() { setInterval(() { setCount(count 1); }, 1000); }, []); return div{count}/div; }这段代码里的count永远不会超过1因为setInterval回调里的count永远是初次渲染时的0。解决方案是给setInterval的回调里使用函数式更新或者把count放进effect的依赖数组中。我建议基础写法一律用函数式更新setCount((c) c 1)。这样不会依赖外部变量也不会因为闭包快照而出错。3.4 依赖数组的必修课什么是你真正需要监听的useEffect的依赖数组是另一个高频踩坑点。依赖数组的作用是告诉你“哪些值变了这个副作用要重新执行一次。”但很多人会倾向于把所有用到的变量都扔进依赖数组这样代码确实不会报lint警告但副作用会频繁执行带来不必要的请求或计算。反过来如果你在useEffect里用了某个props或state却没有把它放进依赖数组React官方会提示你“缺失依赖”。这种警告得认真对待因为它是真实存在的隐患。我习惯的做法是把依赖数组当成“副作用对外部环境的声明”。副作用里引用了什么外部变量就声明什么。如果引用的是一个函数那个函数最好用useCallback包起来避免每次渲染都生成新引用。4. 实操过程从零搭建一次完整的React项目4.1 初始化项目Vite比Webpack聪明在哪这次项目我用Vite作为构建工具而不是老牌Webpack。原因很简单Vite基于原生ES模块开发冷启动速度和热更新速度都比Webpack快一大截尤其适合做Demo级别的小项目几乎不用等待编译。创建项目只需要一条命令npm create vitelatest rea -- --template react-ts这里我选了TypeScript模板。有人可能觉得TS在小项目里是多余的但我的体验是哪怕自己一个人写TS也能极大降低低级错误率。比如接口返回的数据结构用interface定义好之后写每行代码时编辑器都能自动提示字段名省去了切来切去查接口文档的时间。初始化完成后的目录非常干净然后在项目根目录安装依赖cd rea npm install npm run dev看到终端出现Local: http://localhost:5173项目就跑起来了。有的朋友初次用Vite可能遇到“浏览器自动打开但页面空白”的情况通常是Node版本过低导致的不兼容把Node升级到最新LTS版本就好。4.2 开始写代码一个带筛选和分页的表格页面这次项目里我选了一个很常见的后台功能来练手带筛选条件的表格页。这个页面包含搜索框、状态筛选、日期范围选择、表格展示、分页这几个部分。单看设计很简单但操作逻辑其实有难度的地方第一步是数据请求怎么设计。我的做法是用一个统一的请求函数参数包括页码、页大小、筛选条件。每次点击搜索或翻页时组装一个新的查询参数对象发给后端。这里有个很容易犯的错把筛选条件和分页参数混在一起存进state结果改筛选条件时忘了重置页码导致你搜索后停留在第10页而第10页根本不存在。代码里我用了一个独立的filters state来管理筛选条件每次修改filters都强制把currentPage重置为1const [filters, setFilters] useState({ keyword: , status: , dateRange: [] }); const [currentPage, setCurrentPage] useState(1); function handleSearch() { setCurrentPage(1); fetchList(); } async function fetchList() { const params { page: currentPage, ...filters }; const res await getListApi(params); setRows(res.data); setTotal(res.total); }这个模式看着简单但几乎每个后台项目的表格页都是这个逻辑。我建议直接把这段封装成一个可复用的hookusePaginationTable输入是请求函数输出是数据、加载状态、分页控制方法。这样新页面只需一行调用就能拥有完整的表格数据能力。4.3 组件化改造筛选栏和表格如何解耦筛选条件改一下就要重新请求数据但每个页面的筛选条件都长得不一样所以我把筛选栏抽成一个独立的“按键区输入区”组合组件父组件把筛选数据用onFilterChange回调传下去。筛选栏组件内部不关心数据在哪里存储也不关心请求逻辑它只负责渲染表单控件并把用户的输入通过回调上传。这种解耦带来的直接好处是筛选栏可以独立测试。按钮的禁用逻辑、日期范围的前后约束都可以不依赖接口直接在组件内部验证。表格组件则同样只接收dataSource、columns、loading、pagination这些纯展示属性不自己做请求。数据流形成了一个明确的单向链路页面容器 - 请求数据 - 把数据注入表格组件。这一整套下来代码的可维护性比一开始的“一坨”版本提升了不少但代价是需要多建几个文件。我个人认为这种代价非常值得。4.4 权限路由控制用户能看什么页面后台系统里权限路由几乎是标配。这次项目的权限逻辑是这样登录后拿到一个roles数组里面包含当前用户的角色标识。路由配置里每条路由都声明一个requiredRole字段表示访问这个页面需要什么角色。拦截逻辑写在路由守卫里每次切换路由时先检查用户的角色是否匹配。const routeConfig [ { path: /dashboard, element: Dashboard /, requiredRole: admin }, { path: /reports, element: Reports /, requiredRole: [admin, auditor] }, ]; function RequireAuth({ children, requiredRole }) { const { user } useAuth(); if (!user) { return Navigate to/login replace /; } if (!user.roles.some(r requiredRole.includes(r))) { return Navigate to/403 replace /; } return children; }需要注意的一个点是前端权限只是体验优化不是真正的安全手段。只要懂一点技术用户完全可以修改内存数据绕过检查。真正的权限校验必须放在后端接口层面。前端路由守卫的目的是为了避免用户在没有权限的页面上看到一堆报错改善体验。4.5 请求层的设计拦截器与错误统一处理请求层是整个项目里最容易被忽视但影响最大的部分。我用axios做请求库在实例上统一配置了baseURL、超时时间、请求头并加了两层拦截器请求拦截器负责注入token响应拦截器负责统一解包数据和处理HTTP错误。const http axios.create({ baseURL: /api, timeout: 10000 }); http.interceptors.request.use((config) { config.headers.Authorization Bearer ${getToken()}; return config; }); http.interceptors.response.use((response) { if (response.data.code ! 0) { throw new Error(response.data.message); } return response.data.data; }, (error) { if (error.response?.status 401) { clearToken(); window.location.href /login; } ElMessage.error(error.message || 请求失败); return Promise.reject(error); });这层统一处理后业务代码里就不用再写一堆try-catch和if-else判断了。每次调用请求接口直接拿返回值当数据用出错了由拦截器统一弹提示。这个设计让业务代码简化了很多页面组件里聚焦渲染逻辑请求异常从业务逻辑中抽离整个代码看起来非常干净。4.6 性能优化一次真实的列表数据渲染优化项目做到后期表格数据量慢慢增大到几百条时明显感觉到渲染卡顿。排查之后发现两个问题第一我的表格组件在每次点击搜索时都会重新创建columns数组导致子组件因为引用变化而重复渲染第二表格里的某些操作列组件比较重每次渲染都执行了很多非必要的计算。解决办法是把columns定义放在组件外部用useMemo包裹那些由props计算而来、比较重的数据。操作列里放的是一个“确认删除”的按钮这个按钮的确认弹窗状态被提到表格组件内部避免在数据map过程中创建函数引用。改进之后相同数据量下渲染时间明显缩短。对于几百条数据的表格来说其实不需要虚拟滚动但前提是避免不必要的重复渲染。5. 常见问题与排查技巧实录5.1 状态更新了页面为什么没变这个问题我见过太多次包括我自己早期也经常犯。遇到这种情况先别急着怀疑React而是检查三件事第一你是不是调用了set函数但传入了同一个值的引用。React会对下一次渲染前进行浅比较如果值没有变化它不会触发更新。比如你有一个数组state你执行了arr.push(newItem)然后setArr(arr)因为引用没变React觉得没有变化于是页面不刷新。正确做法是setArr([...arr, newItem])新建一个数组引用。第二你是不是在一个事件处理函数里连续调用了两次set函数且都基于旧值。React的set函数传入一个新的值时是基于当前渲染的旧值计算而不是你上一次set后的值。如果你需要基于最新值连续更新两次务必使用函数式更新写法。第三你是不是在useEffect里设置了一个本身会触发该effect重跑的值造成死循环。一个典型的例子是在effect里请求接口返回值又作为依赖数组的元素结果请求完数据变了effect又跑一遍接口被无限调用。这种情况需要检查依赖数组把不该作为依赖的值去掉或者通过条件判断避免重复请求。5.2 key警告怎么消除不要再盲目用indexReact列表渲染时要求给子元素加key属性这个key的作用是帮助React识别哪些元素被修改、增加或删除。很多人图省事直接写key{index}这在纯展示型静态列表里勉强能跑但只要列表数据会发生增删改排序index就会引发严重的bug。举一个典型的案例你有两个输入框分别对应数组里的两个元素。你把key设成了index那么当用户删掉第一个输入框时列表自动重新渲染。5.3 重复渲染为什么React会“多做”很多工作React的每次setState都会触发当前组件重新渲染这是它的基本机制。如果整个应用的使用状态区域范围很广就会造成很多与本次更新无关的组件也跟着渲染页面性能自然下降。我这次项目里就用React DevTools的profiler工具测过一次发现有一个列表组件因为接收了父组件的整个filters对象哪怕只是改了一个筛选框的值整个列表都会重新渲染。但没有加载状态用户感受不明显等数据量拉上去之后必然卡顿。最简单的优化方案是给子组件包一层React.memo让它在props不变时跳过重复渲染。但需要注意React.memo只做浅比较如果你在父组件里内联定义了一个对象或函数并传给子组件子组件每次都会认为props变了memo就失效了。所以使用memo必须同时注意传参的引用稳定性。5.4 useEffect依赖警告痛并成长着依赖数组的警告很折磨人但它是React在试图帮你。每次lint报“React Hook useEffect has missing dependency”时我的处理步骤一般是先看这个依赖是不是必须的。如果一个函数在effect内部被调用但它不依赖外部变量那我直接在effect内部定义这个函数不放到外面。如果这个函数依赖外部变量那就使用useCallback包裹函数并把这函数放进依赖数组。如果依赖的是一个props对象但effect只需要其中某个字段那依赖数组就只写这个字段避免对象引用变化导致effect多余执行。还有一个小技巧如果某次effect真的只想在挂载时执行一次但lint总报依赖缺失你可以先问自己“这个变量在组件挂载之后还会不会变化变化了到底要不要重新跑一次effect”。大多数情况下答案是需要重新执行所以lint是对的。只有极少数场景比如你确实只想要初始值而后续变化不应影响逻辑才值得考虑通过useRef手动处理。5.5 请求竞态返回顺序错乱导致表格数据错误听上去很高级的坑其实一句话就能说清用户点击第1页然后快速点击第2页两次请求同时发出。如果第1页的请求比第2页先发出但后返回表格里显示的就是第1页数据而不是用户当前所在的第2页。我在这次项目里就用AbortController解决了这个竞态useEffect(() { const controller new AbortController(); fetchList({ signal: controller.signal }); return () controller.abort(); }, [currentPage, filters]);在effect的清理函数里执行abort相当于在下一次effect启动前取消上一次未完成的请求。这样即使请求乱序返回旧请求的结果也不会被写入表格。这个处理方式还能顺便解决组件卸载后更新state导致的内存泄漏警告。6. 一点个人体会这次做“rea”这个项目最大的收获不是我写了多少行代码而是我终于把React的开发节奏从“随手写”调成了“先想后写”。最早学React的时候我也是打开编辑器就敲想到什么写什么结果一个页面改需求能改到心态崩溃。后来发现几乎所有代码混乱的根源都出在设计阶段组件边界没想清楚、状态放错了层级、依赖关系没理顺。这些问题事后修都很麻烦但动手前花半小时理一理后面能省出一整天的调试时间。React是一门越用越觉得“前面有坑”的技术栈但恰恰是这些坑让人成长。希望这份记录能帮正在做React项目的你少走几个弯路尤其是那些依赖数组、闭包陷阱、请求竞态看着高大上实际解决起来无非是几个规范动作。先把基础套路吃透再谈扩展和优化这条路基本不会错。
阅读完成 · 觉得有帮助?