当我们强调“组件 Render 阶段必须纯净”时,很多刚接触 Hooks 的开发者会产生困惑:如果不写在组件函数体里,我的业务逻辑到底该往哪放?
核心的秘密在于:我们需要把“业务逻辑”分类。并不是所有业务逻辑都是“副作用”。在 React 中,业务逻辑根据其性质,有四个完美的去处。
业务逻辑的四大分类与去处
1. 纯计算逻辑(计算衍生状态)→\rightarrow→直接写在函数体内
如果你的业务逻辑是根据现有的 Props 或 State 计算出一个新数据(例如:对列表进行搜索过滤、格式化时间、计算总价),这属于纯计算,没有任何副作用。
- 怎么写:直接写在组件函数体内。每次渲染重新计算是完全可接受的(如果计算量极大,才用
useMemo包裹)。 - ❌ 错误做法:用
useEffect监听状态变化,然后去setFilteredList(这会导致二次渲染)。
functionProductList({products,filterKeyword}){// ✅ 正确:纯计算逻辑,直接写在函数体内constfilteredProducts=products.filter(p=>p.name.includes(filterKeyword))consttotalPrice=filteredProducts.reduce((sum,p)=>sum+p.price,0)return<div>总价:{totalPrice}</div>}2. 用户触发的业务(数据提交/修改)→\rightarrow→写在事件处理函数或 React 19 Actions 中
如果你的业务逻辑是因为用户点了某个按钮、提交了表单才触发的(例如:删除商品、点赞、提交注册),这属于主动意图。
- 怎么写:写在
onClick、onSubmit等事件回调函数内部,或者利用 React 19 的Actions(结合useTransition)来处理异步提交。 - 特点:这些函数只在事件发生时调用,绝对不会在 Render 阶段自动执行,因此你可以安全地在里面写任何副作用(如
fetch、修改全局状态)。
functionDeleteButton({id}){const[isPending,startTransition]=useTransition()consthandleDelete=()=>{// ✅ 正确:在事件触发的 Action 中编写异步业务逻辑startTransition(async()=>{awaitfetch(`/api/delete/${id}`,{method:'POST'})// 更新状态...})}return(<button onClick={handleDelete}disabled={isPending}>删除</button>)}3. 被动同步逻辑(依赖外部系统)→\rightarrow→写在useEffect或 React 19use()中
如果你的业务逻辑是组件一旦加载出来,或者某个依赖变了,就必须自动去干的一件事(例如:进入页面自动埋点、根据用户 ID 自动获取详情、订阅 WebSocket)。
- 怎么写:写在
useEffect中,或者在 React 19 中使用use(Promise)进行声明式的数据流读取。
useEffect(()=>{// ✅ 正确:属于被动同步的副作用,放在 useEffect 中consttracker=newAnalyticsTracker()tracker.sendPageView()return()=>tracker.disconnect()// 别忘了清理},[])4. 复杂的跨组件业务→\rightarrow→抽离到自定义 Hook(Custom Hook)中
当一个组件里塞满了大量的状态、事件处理、和计算,代码变得臃肿时,最优雅的解决方案是把业务逻辑彻底抽离出 UI 组件。
- 怎么写:创建一个以
use开头的自定义 Hook,把useState、useEffect、各种计算函数都打包塞进去,组件只负责看图说话(渲染 UI)。
// 📦 独立的业务逻辑层 (useCart.js)functionuseCart(){const[items,setItems]=useState([])consttotalPrice=items.reduce((sum,i)=>sum+i.price,0)constaddItem=item=>setItems([...items,item])return{items,totalPrice,addItem}}// 🎨 纯粹的 UI 渲染层 (CartComponent.js)functionCartComponent(){// 组件体内只有一行干净的解构,没有任何杂乱的业务逻辑const{items,totalPrice,addItem}=useCart()return(<button onClick={()=>addItem({price:10})}>加购({totalPrice})</button>)}总结速查表
| 业务逻辑类型 | 举例 | 应该写在哪里? | 是否允许副作用? |
|---|---|---|---|
| 衍生数据计算 | 过滤列表、计算总和、格式化文本 | 组件函数体内 | ❌ 绝对不行(必须是纯函数) |
| 用户交互响应 | 点击保存、删除、切换开关 | 事件处理函数 / Actions | ✅ 可以 |
| 外部系统同步 | 页面加载取数、监听窗口大小 | **useEffect/use()** | ✅ 可以 |
| 复杂/复用业务 | 完整的购物车逻辑、分页器逻辑 | 自定义 Hooks | 视内部的具体 Hooks 而定 |
把 UI(怎么画)和业务(怎么算/怎么变)通过上述规则清晰地解耦,就是写出高质量、可维护 React 19 代码的关键。