先记住这个答案
事件处理器知道用户执行了什么动作,可以携带这次动作的参数并处理成功、失败和后续步骤。Effect 则根据响应式输入变化重新同步,并不天然知道变化是由哪次用户意图引起。提交订单或发送反馈若绕成设置一个布尔值再由 Effect 发请求,会增加中间状态和重复触发边界。事件中可以更新 state,随后需要与该状态保持同步的外部资源仍可交给独立 Effect 管理。
- 一次动作的因果关系放在事件流程中
- 持续匹配当前状态的资源交给同步流程
- 状态变化与外部操作之间不要增加无意义的触发标记
发送反馈不需要经过一个中间布尔状态
假设用户填写意见后点击发送。事件处理器可以读取本次输入,建立请求状态,等待结果并显示成功或错误。如果先 setShouldSend(true),再让 Effect 观察它发送,就还要解释何时重置、第二次点击是否改变值、输入改变是否应该再次发送等额外问题。
特别是依赖同时包含输入内容时,发送标记仍为 true 的期间修改文本可能触发另一轮请求;为了阻止它而删除输入依赖,又可能使用旧内容。把命令放回事件流程,能直接保留本次提交参数,并把按钮禁用、重复提交和服务端幂等规则表达在同一个业务动作中。
打开详情之后,有些工作仍然适合 Effect
点击某商品可以只更新 selectedId,页面根据它展示详情。若外部实时服务需要持续订阅当前商品库存,那么订阅应与当前 selectedId 保持一致,无论这个值来自点击、路由恢复还是父组件传入,都应切换到正确的资源。这类关系适合由 Effect 建立和清理。
因此同一次交互可以同时涉及事件与同步,但职责不同。事件说明用户选择了什么;Effect 说明组件当前展示这个对象时,外部连接应该是什么。不要把建立连接的代码复制到所有可能修改 selectedId 的事件里,否则新增一个入口时很容易漏掉同步。
用可恢复的业务状态表达异步过程
事件处理器中执行异步操作仍需要处理竞争和错误,例如两次提交是否允许并行、页面离开后怎样显示结果、请求失败后是否保留输入。事件位置本身不保证只执行一次,重复点击和网络重试依然可能发生。关键写入应有清楚的请求标识与服务端幂等策略。
自动保存则要根据产品语义判断:输入停顿后保存当前草稿是一种持续同步需求,可以使用带取消与版本检查的同步机制;用户明确点保存按钮则属于一次事件。两者可以共存,但需要区分保存版本,避免自动保存的迟到结果覆盖用户刚刚确认的内容。
容易答错的地方
- 所有请求都应该放在 useEffect
- 请求只是外部操作的一种实现,放置依据是触发原因。用户命令、框架数据加载和持续同步各有合适位置,不能只看函数名里是否包含 fetch。
- 移到点击事件就天然不会重复提交
- 用户仍可以重复点击,客户端或网络层也可能重试。需要明确提交状态、去重范围与服务端语义,不能用代码位置代替可靠的业务防重。
面试官还会怎么问?
事件处理器需要放在 Effect 依赖数组里吗?
如果 Effect 不读取它,就没有这项依赖。不要为了共享一段逻辑让事件和 Effect 相互调用;可以抽取普通纯函数或明确的服务操作,再分别组织触发流程。
页面一显示就需要建立连接,算事件吗?
如果要求连接持续匹配当前展示的参数,无论页面如何进入都应生效,这更像同步关系。Effect 应配对清理,保证参数切换和页面离开时资源正确释放。
点击后计算合计需要 Effect 吗?
若合计只是当前商品和数量的纯函数,可以在渲染时计算。点击事件更新数量即可,不必再保存一份合计并用 Effect 同步,否则会增加可过期的冗余状态。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。