在React生态系统中,数据获取是一个永恒的话题。随着Hooks的普及,开发者们纷纷编写自定义的useFetch Hook来封装网络请求逻辑,但一个关键问题始终悬而未决:获取到的数据到底该不该放进React的State中? 这个看似简单的问题,实则牵涉到组件渲染性能、状态管理哲学乃至应用架构设计。本文将深入剖析这一争议,并给出可操作的实践建议。
问题的起源:经典做法与隐性成本
绝大多数React开发者第一次接触数据获取时,都会写出类似这样的代码:
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/api/items')
.then(res => res.json())
.then(json => setData(json))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, []);
这种做法将数据、加载状态、错误状态全部放入useState,看似直观。然而,这背后隐藏着几个容易被忽略的问题:
- 不必要的重渲染:每次
setData都会触发组件及其子组件的重新渲染,即使数据并没有实质变化(例如服务端返回了相同的数据)。 - 状态冗余:如果数据已经通过
useRef或外部缓存(如React Query、SWR)管理,再将其存入useState就造成了内存和计算的双重浪费。 - 竞态条件:当组件快速卸载又挂载(如路由切换)时,旧请求的响应可能覆盖新数据。
反对放入State的声音:缓存与解耦
以TanStack Query(原React Query) 和SWR为代表的库,提供了完全不同的思路:数据应该由专门的缓存层管理,而不是组件的局部状态。
这些库的核心思想是:
- 数据是资源而非状态。组件只是数据的消费者,不负责存储。
- 缓存层会自动处理去重、后台刷新、乐观更新等复杂逻辑。
- 组件通过
useQuery或useSWRHook获取数据时,得到的返回值实际上是缓存的快照,而非React State。
比如使用SWR:
const { data, error } = useSWR('/api/items', fetcher);
// 这里data的变化由SWR内部触发,但并非组件State
这种模式的优势明显:多个组件请求相同URL时只发起一次网络请求;数据过期后自动重新验证;页面切换后数据可被保留。更重要的是,State的更新不再与网络请求直接挂钩,开发者可以更专注于UI逻辑。
支持放入State的场景:简单与可控
然而,并非所有场景都需要引入重型缓存库。对于小型应用、原型开发或数据更新频率极低的情况,将数据放入useState依然合理。支持者提出以下论据:
- 零额外依赖:不用安装第三方库,项目体积更小。
- 直观性:团队新手更容易理解“数据就是状态”的模型。
- 完全控制:可以手动控制数据更新的时机和方式,避免缓存库的黑盒行为。
此外,当数据需要被多个组件同步更新,并且这些组件不是通过全局缓存共享时,放入父组件的State反而是最直接的方式。例如,一个表单在提交后需要在不同子组件中反映新数据,用useState配合回调函数就能简单实现。
社区争议与权衡
在Stack Overflow、Reddit和Twitter上,围绕这个问题的争论从未停止。知名开发者Dan Abramov曾表示:“State是用于随时间变化的信息,而数据通常是通过请求获取的快照,两者概念不同。” 他的观点倾向于将数据与状态分离。
但实践中,很多React教程仍将useState + useEffect作为标准范式。问题在于,这种模式容易导致过时渲染:当组件卸载后,setData被调用会在控制台抛出警告,并可能引发内存泄漏。虽然可以使用AbortController取消请求,但代码复杂度也随之上升。
最佳实践:三层决策框架
综合各方观点,我们可以构建一个决策框架,帮助开发者根据场景选择策略:
第一层:数据生命周期
- 如果数据只在当前组件内使用,且组件卸载后无需保留(如详情页的一次性加载),可以使用useState + 手动处理竞态条件。
- 如果数据需要在不同页面或组件间共享(如用户信息、全局配置),强烈建议使用useRef或外部缓存库。
第二层:更新频率与副作用
- 对于实时更新(如WebSocket推送、轮询),SWR/React Query能自动处理间隔刷新,而手动State需要额外编写定时器逻辑。
- 对于一次性加载(如博客文章内容),useState足够,但需注意加上错误边界和取消机制。
第三层:团队规模与演进
- 小型Side Project或原型阶段,直接用useState快速迭代。
- 大型生产应用,尤其是涉及复杂数据依赖关系时,尽早引入专业的缓存库。
结论:没有银弹,但有倾向
回到标题的问题:应该把数据放进State吗? 答案取决于上下文。但业界主流趋势已逐渐清晰:将获取的数据与组件State解耦,通过专门的缓存层或全局状态管理工具处理。这不仅能提升性能,还能让代码更易维护。
即使你坚持使用自定义的useFetch Hook,也建议考虑以下改良版本:
function useFetch(url) {
const dataRef = useRef(null);
const [, forceUpdate] = useReducer(x => x + 1, 0);
useEffect(() => {
fetch(url).then(res => res.json()).then(newData => {
dataRef.current = newData;
forceUpdate(); // 仅当数据变化时触发渲染
});
}, [url]);
return dataRef.current;
}
这种实现通过useRef暂存数据,只在实际变化时触发最小范围的渲染,兼顾了简单与效率。当然,如果项目允许,直接拥抱SWR或React Query会是更省心的选择。
在React的世界里,没有绝对正确的答案,只有最适合场景的方案。理解数据与状态的本质区别,才是写出健壮应用的关键。