React中useMemo缓存动态组件后重渲染仍执行useState初始逻辑的原因
问题根因拆解
核心逻辑分层
你观察到的每次App强制更新都触发getData called的现象,是两个非常常见的React使用误区叠加导致的,和useMemo的缓存失效、useState的执行时机直接相关:
- 关于
useState的执行时机误区
很多人以为传给useState的初始值只会在组件首次挂载时执行一次,这其实是对JS求值规则的误解:当你写
useState(getData())时,getData()这段代码的执行时机,是Comp组件函数体执行到这一行语句的瞬间。只要Comp没有被React卸载、没有因为组件引用变化被判定为需要重新挂载,那么Comp的函数体只会在首次渲染时执行到这行代码,后续重渲染时哪怕逻辑上走到useState调用,也不会重新触发getData的执行。 - 关于
useMemo的作用边界误解useMemo的本质只是在依赖值不变时,缓存上一次的计算结果,它从来不是用来“永久保持组件引用稳定”或者“阻止组件重渲染”的万能工具:- 如果
useMemo的依赖数组里包含了每次重渲染都会变化的非稳定值(比如内联对象、内联数组、未做缓存的函数),那么useMemo每次都会重新执行工厂逻辑,生成一个全新的Comp组件引用。 - React在做组件树diff的时候,只要发现同一渲染位置的组件引用和上一次不一致,就会直接卸载旧的组件实例,挂载全新的组件实例。这个时候Comp的整个函数体会从头执行,自然就会跑到
useState(getData())的代码行,触发控制台的日志打印。 - 就算
useMemo的依赖完全稳定、Comp引用没有变化,只要你的RenderWidget组件本身没有被React.memo包裹,App触发强制更新时RenderWidget的函数体一定会重新执行——这种场景下因为Comp引用稳定,React不会重复挂载Comp,也就不会触发getData的打印。
- 如果
最常见的踩坑写法
你现在遇到的问题,大概率是下面的错误写法导致的:
function RenderWidget() { // 注意:这个配置对象每次组件渲染都是全新的引用 const widgetConfig = { id: "test-widget" }; // useMemo依赖了不稳定的配置对象,缓存完全失效 const Comp = useMemo(() => { return () => { const [data, setData] = useState(getData()); return <div>{data}</div> } // 因为widgetConfig每次都是新引用,useMemo永远认为依赖变了 }, [widgetConfig]); return <Comp />; }
这种写法下,每次App触发更新、RenderWidget重新执行的时候,widgetConfig都是新的对象引用,useMemo每次都会重新创建一个全新的Comp组件,React自然会反复卸载、重挂Comp,导致getData被反复调用。
快速验证方法
你可以在Comp组件内部加一个空依赖的挂载生命周期日志:
useEffect(() => { console.log("Comp mounted"); }, [])
如果每次点击H1都能看到Comp mounted的日志,就可以100%确认是Comp组件引用不稳定、被反复重挂导致的问题,和useState本身的机制无关。
内容的提问来源于stack exchange,提问作者Adrián
相关产品推荐
相关产品推荐

