开发原则:按需调用接口,及时反馈状态
好的产品实现不应该一上来就把所有重数据接口都打满,也不应该让用户操作后停在无反馈状态。接口调用要跟随用户真实交互,UI 状态要清楚表达进行中、成功和失败。
- Product
- Engineering
- UX
- Frontend
- React
有些开发原则看起来很小,但会直接影响一个产品用起来是轻还是重、是可信还是让人不安。
我最近会特别强调两个原则:
- 按需调用接口,不要一上来就请求重数据
- UI 操作后必须给用户反馈
它们一个影响系统负载和页面响应,一个影响用户对操作结果的判断。
一、接口调用要跟着用户交互走
很多页面一打开,就把所有可能用到的数据都请求回来。
这种做法看起来省事,问题也很明显:
- 首屏变慢
- 后端压力变大
- 很多数据其实用户根本不会看到
- 页面逻辑更难判断哪些数据是真正必要的
- 一旦某个重接口失败,可能拖累整个页面体验
更好的默认策略是:
用户交互到哪里,接口再按需要触发到哪里。
比如一个页面里有多个详情面板、统计图、历史记录和高级配置,不一定要在页面初始化时全部请求。
更合理的方式通常是:
- 首屏只加载用户马上需要看的核心数据
- 用户展开详情时,再请求详情数据
- 用户切换到某个 tab 时,再请求这个 tab 对应的数据
- 用户触发分析、生成、导出这类重操作时,再调用对应接口
- 已经请求过且仍然有效的数据,可以按策略缓存
这样做不是为了少写代码,而是为了让系统行为更接近真实使用路径。
用户没有进入某个区域,就不应该让这个区域提前消耗大量资源。
尤其是重数据接口、长耗时接口、付费资源接口、会触发复杂计算的接口,更不应该在页面加载时无差别调用。
这里的判断标准很简单:
接口调用应该服务于当前用户意图,而不是服务于开发时的省事。
如果一个数据不是首屏决策所必需,也不是当前交互马上要展示的结果,就应该先问一句:它是否真的需要现在请求?
二、UI 操作后要给出反馈
另一个很容易被忽略的问题,是用户点了按钮、提交了表单、触发了任务,但界面没有任何明确反馈。
这会让用户不知道:
- 点击是否生效
- 请求是否正在进行
- 操作是否已经成功
- 失败后应该怎么处理
- 能不能重复点击
所以任何会产生状态变化的 UI 操作,都应该给出反馈。
最基本的反馈包括三类:
- 进行中
- 成功
- 失败
进行中状态要告诉用户操作已经被接收,并且系统正在处理。
例如:
- 按钮进入 loading
- 表单提交期间禁用重复提交
- 长任务显示进度或当前阶段
- 后台任务提示“已开始处理”
成功状态要告诉用户结果已经生效。
例如:
- 保存成功
- 已复制
- 已提交
- 已创建任务
- 已更新配置
失败状态要告诉用户哪里失败了,以及下一步能做什么。
例如:
- 网络异常,请稍后重试
- 参数不完整,请补充必填项
- 权限不足,请联系管理员
- 任务启动失败,可以重新提交
这里最差的体验不是失败,而是失败了却没有反馈。
因为没有反馈时,用户只能猜。
他可能会反复点击,可能会刷新页面,可能会以为系统卡死,也可能会在不知道结果的情况下继续做后续操作。
所以 UI 状态不是装饰,它是产品行为的一部分。
一个操作是否可信,不只取决于后端有没有成功处理,也取决于前端有没有把处理状态清楚表达出来。
三、React 里的数据请求要有默认护栏
如果页面是 React 实现,按需调用接口还要进一步处理 Effect、竞态和缓存问题。
React 文档里有几个很关键的建议。
第一,Effect 里请求数据时,要有 cleanup 或 ignore 机制。
因为依赖变化、组件卸载、用户快速切换条件时,旧请求可能比新请求更晚返回。如果旧请求返回后还继续 setState,页面就可能把过期数据写回当前状态。
React 文档里的典型做法是在 Effect 内部放一个 ignore 标记,并在 cleanup 里把它改掉:
useEffect(() => {
let ignore = false;
async function startFetching() {
const result = await fetchData(requestKey);
if (!ignore) {
setData(result);
}
}
startFetching();
return () => {
ignore = true;
};
}, [requestKey]);
这个写法的重点不是 ignore 这个变量本身,而是原则:
旧请求不能回写新状态。
第二,开发环境的 Strict Mode 会故意执行 setup -> cleanup -> setup。
这意味着 Effect 逻辑必须能承受重复挂载、重复清理、重复请求。
不要假设“组件只会挂载一次”,也不要靠“开发环境多请求一次没关系”来掩盖问题。React 这么做,是为了让缺失 cleanup、重复订阅、资源泄漏、竞态回写这些问题更早暴露出来。
第三,不要手写一堆分散的 Effect 请求逻辑。
如果每个组件都自己写 useEffect + fetch + loading + error + ignore + cache,最后很容易变成:
- 请求逻辑重复
- loading 和 error 表达不一致
- 缓存策略不一致
- 同一个接口被多个组件重复请求
- 竞态问题散落在各处
React 文档更推荐优先使用框架的数据加载能力,或者使用、构建 client-side cache。
例如:
- React Router loader
- TanStack Query
- SWR
- 框架内置的数据加载机制
这些方案通常会统一处理缓存、去重、预加载、请求状态和竞态问题,比在每个组件里散写 Effect 更稳。
第四,如果自己实现缓存,要按 request key 去重,并复用 in-flight promise。
同一个请求 key 如果已经在请求中,就不应该再打一次网络,而应该复用当前还没完成的 promise。React Suspense 文档里的缓存示例,本质也是用 Map<url, promise> 这类结构来记录请求:
const cache = new Map<string, Promise<unknown>>();
function fetchWithCache(key: string) {
if (!cache.has(key)) {
cache.set(key, fetchData(key));
}
return cache.get(key);
}
真实项目里,这个 key 不能只随便拼一个 URL,而应该能稳定表达一次请求的身份。
例如:
- endpoint
- query 参数
- 当前用户或租户
- 分页参数
- filter 和 sort
这样才能避免两个问题:
- 同一个请求被重复触发,浪费网络和计算资源
- 不同请求被错误复用,导致数据显示串了
所以 React 页面里的接口调用,不只是“什么时候请求”的问题,还包括:
- 旧请求能不能被安全忽略
- 重复 Effect 会不会破坏状态
- 请求状态是否集中管理
- 相同 request key 是否能复用 in-flight promise
这几件事处理好了,按需调用才不会变成一堆脆弱的局部 Effect。
四、这两个原则其实是一件事
按需调用接口,解决的是系统不要提前做不必要的事。
操作后给出反馈,解决的是系统做了事之后要让用户知道。
前者要求开发者尊重用户真实路径。
后者要求开发者尊重用户当前感受。
放在一起看,它们都指向同一个原则:
系统行为要被用户意图驱动,也要被用户感知到。
如果页面还没交互,就把所有接口都打满,这是系统抢跑。
如果用户已经操作了,界面却没有状态反馈,这是系统失语。
好的产品实现应该避免这两件事。
它应该在用户需要时再行动,并且在行动后及时告诉用户发生了什么。