为何在useEvent垫片中使用useLayoutEffect而非直接在渲染阶段设置ref?
先来看一种常见的useEvent垫片实现:
function useEvent(callback) { const ref = useRef(() => { throw new Error("Cannot call an event handler while rendering.") }) // React 18 也可以用 useInsertionEffect useLayoutEffect(() => { ref.current = callback }) return useCallback((...args) => ref.current(...args), []); } // 使用示例 function Component(props) { const [visible, setVisible] = useState(false) // props.onToggle 可能不稳定 const onToggle = useEvent(props.onToggle) // 但我们的 onToggle 是稳定的 useEffect(() => onToggle(visible), [onToggle, visible]) // ❌ 在渲染阶段调用会抛出错误 onToggle(visible) }
我困惑的点在于:为什么此处需要使用useLayoutEffect,而不能直接在渲染函数中设置ref的值?比如下面这种实现:
function useEvent(callback?) { const ref = useRef(callback) ref.current = callback return useCallback((...args) => ref.current(...args), []) }
核心原因:规避渲染阶段的不一致性与意外行为
防止渲染期间读取到过时/交替的回调
直接在渲染函数里设置ref.current = callback,会导致组件一次渲染周期内ref值可能被多次修改(比如组件触发多次重渲染时)。如果渲染过程中有其他逻辑读取这个ref,就可能交替拿到新旧回调,引发难以排查的状态不一致问题。而useLayoutEffect是在DOM更新完成后、浏览器绘制前同步执行的,能保证ref在渲染完成后才被更新,确保后续事件触发或effect调用拿到的都是最新回调。遵循React渲染的纯函数原则
React要求渲染过程是纯函数,不能包含隐性副作用。直接在渲染里修改ref属于违反这个原则的操作——虽然ref修改不会触发重渲染,但会打破渲染的可预测性。比如在严格模式下,React会执行两次渲染检测副作用,直接修改ref会导致两次渲染都变更ref值,引发不符合预期的状态波动。而useLayoutEffect是React官方定义的处理DOM相关副作用的钩子,完全符合框架设计规范。保证初始错误提示的有效性
原垫片里初始ref设置了一个抛出错误的函数,目的是禁止在渲染阶段调用事件处理函数。如果直接在渲染里设置ref,第一次渲染时ref.current就会被替换成传入的callback,此时在渲染阶段调用onToggle就不会触发错误提示,失去了这个防护机制。而useLayoutEffect是在渲染完成后才更新ref,渲染阶段调用时会触发初始ref里的错误,达到预期的警示效果。
内容的提问来源于stack exchange,提问作者François Zaninotto

