React中自定义useWatch1与useWatch2表现不符的原因探究
我实现了两个自定义Hook useWatch1 和 useWatch2,它们的作用是当监听值变化时执行指定的回调函数cb。
操作流程:
- 多次点击
ADD COUNT按钮使count值增加 - 再点击
ADD ID按钮,此时预期count值被设为0
实际结果:useWatch2符合预期,但useWatch1无法实现该效果,请问原因是什么?
代码示例
import { useRef, useState } from "react"; function useWatch1(current, cb) { const isFirstRender = useRef(true); const prevRef = useRef(null); if (isFirstRender.current) { prevRef.current = current; isFirstRender.current = false; } else if(prevRef.current !== current){ prevRef.current = current; cb(); } } function useWatch2(current, cb) { const [prev, setPrev] = useState(current); if (current !== prev) { setPrev(current); cb(); } } function MyCpm({ id }) { const [count, setCount] = useState(0); // useWatch1(id, () => { // setCount(0); // }); useWatch2(id, () => { setCount(0); }); console.log("render", count); return ( <> <button onClick={() => { setCount((c) => c + 1); }} > ADD COUNT </button> <span>count: {count}</span> <span>-----</span> <span>id: {id}</span> </> ); } function App() { const [id, setId] = useState(0); return ( <> <button onClick={() => { setId((id) => id + 1); }} > ADD ID </button> <MyCpm id={id}></MyCpm> </> ); } export default App;
原因分析
useWatch1失效的核心问题
useWatch1直接在组件渲染阶段执行了状态更新回调cb()(也就是setCount(0)),这违反了React的渲染规则:渲染过程中不能触发状态更新。
当点击ADD ID导致MyCpm因id变化重新渲染时,useWatch1检测到id变化后立刻调用cb(),触发setCount(0)。这会让组件马上进入新一轮渲染,但此时prevRef.current已经被更新为新的id,新一轮渲染中useWatch1不会再执行回调,而这次状态更新会被React的批处理机制干扰,最终导致count没有被正确重置。
另外,useWatch1把副作用逻辑写在渲染阶段的if分支里,完全不符合React的Hook设计原则——副作用必须放在useEffect这类专门的副作用Hook中执行,不能直接嵌入渲染流程。
useWatch2能生效的原因
useWatch2用useState保存之前的值,当current !== prev时先调用setPrev(current)触发组件重渲染,同时执行cb()。虽然这种写法也有瑕疵(可能触发两次渲染),但setPrev和setCount会被React合并批处理,最终cb()里的setCount(0)能被正确执行,实现count重置的效果。
正确的useWatch实现方式
要实现稳定的监听值变化执行回调,应该用useEffect处理副作用,这是React官方推荐的方式:
function useWatch(current, cb) { const prevRef = useRef(current); useEffect(() => { if (prevRef.current !== current) { cb(); prevRef.current = current; } }, [current, cb]); }
这个版本把监听逻辑放在useEffect中,确保是在组件渲染完成后执行副作用,不会干扰渲染流程,能稳定监听值的变化并执行回调。
内容的提问来源于stack exchange,提问作者user15163984

